diff --git a/www/js/controllers/feed-controllers.js b/www/js/controllers/feed-controllers.js index 6907ff3b118d66775b8f8dcf70ecef52a32e1462..a7c9f0ff7b4dd13d2af25b677763ac4321df247f 100644 --- a/www/js/controllers/feed-controllers.js +++ b/www/js/controllers/feed-controllers.js @@ -28,9 +28,6 @@ function FeedController($scope, $timeout, $http, $translate, $q, csConfig, csHtt return null; }) .then(function(feed) { - // Clean title (remove duplicated titles) - $scope.prepareJsonFeed(feed); - $scope.loading = false; $scope.feed = feed; var showFeed = feed && feed.items && feed.items.length > 0 || false; @@ -49,15 +46,21 @@ function FeedController($scope, $timeout, $http, $translate, $q, csConfig, csHtt var minDate = maxAgeInMonths > 0 ? moment().subtract(maxAgeInMonths, 'month').startOf('day').utc() : undefined; $scope.search.minTime = minDate && minDate.unix(); + // Reset load counter + $scope.search.loadCount = 0; + var now = Date.now(); console.debug("[feed] Loading from {url: {0}, minTime: '{1}'}".format(feedUrl, minDate && minDate.toISOString() || 'all')); - return $scope.getJsonFeed(feedUrl) + return $scope.loadJsonFeed(feedUrl) .then(function (feed) { console.debug('[feed] {0} items loaded in {0}ms'.format(feed && feed.items && feed.items.length || 0, Date.now() - now)); if (!feed || !feed.items) return null; // skip + // Clean title (remove duplicated titles) + $scope.cleanDuplicatedTitles(feed); + return feed; }) .catch(function(err) { @@ -66,26 +69,30 @@ function FeedController($scope, $timeout, $http, $translate, $q, csConfig, csHtt }); }; - $scope.getJsonFeed = function(feedUrl, maxCount) { + /** + * Load a JSON file, from the given URL, and convert into the JSON Feed format + * @param feedUrl + * @param maxCount + * @returns {*} + */ + $scope.loadJsonFeed = function(feedUrl, maxItemCount) { var locale = $translate.use(); - maxCount = maxCount || $scope.search.maxCount; var minTime = $scope.search.minTime; + maxItemCount = maxItemCount || $scope.search.maxCount; + // Increment load counter (to avoid infinite loop) $scope.search.loadCount++; + return $scope.getJson(feedUrl) .then(function(json) { - // Detect Discourse category, then convert - if ($scope.isDiscourseCategory(json)) { - return $scope.parseDiscourseCategory(feedUrl, json); + // Parse JSON from discourse + if ($scope.isJsonDiscourse(json)) { + return $scope.parseJsonDiscourse(feedUrl, json); } - // Detect Discourse topic, then convert - else if ($scope.isDiscourseTopic(json)) { - return $scope.parseDiscourseTopic(feedUrl, json); - } - - return json; + // Return a copy (to avoid any change in cached data) + return angular.copy(json); }) .then(function(feed) { if (!feed || !feed.items && !feed.next_url) return null; // skip @@ -115,6 +122,7 @@ function FeedController($scope, $timeout, $http, $translate, $q, csConfig, csHtt return res.concat(item); }, []); + return feed; }) .then(function(feed) { @@ -122,23 +130,23 @@ function FeedController($scope, $timeout, $http, $translate, $q, csConfig, csHtt feed.items = feed.items || []; // Slice to keep last (more recent) items - if (feed.items.length > maxCount) { - feed.items = feed.items.slice(feed.items.length - maxCount); + if (feed.items.length > maxItemCount) { + feed.items = feed.items.slice(feed.items.length - maxItemCount); return feed; } // Not enough items: try to fetch more var canFetchMore = feed.next_url && feed.next_url !== feedUrl && $scope.search.loadCount < $scope.search.maxLoadCount; - if (canFetchMore && feed.items.length < maxCount) { + if (canFetchMore && feed.items.length < maxItemCount) { console.debug("[feed] Loading from {next_url: '{0}'}".format(feed.next_url)); // Fetch more - return $scope.getJsonFeed(feed.next_url, maxCount - feed.items.length) + return $scope.loadJsonFeed(feed.next_url, maxItemCount - feed.items.length) .then(function(moreFeed) { // Append new items if (moreFeed && moreFeed.items && moreFeed.items.length) { - feed.items = feed.items.concat(moreFeed.items.slice(0, maxCount - feed.items.length)); + feed.items = feed.items.concat(moreFeed.items.slice(0, maxItemCount - feed.items.length)); } return feed; @@ -147,8 +155,13 @@ function FeedController($scope, $timeout, $http, $translate, $q, csConfig, csHtt return feed; }); - } + }; + /** + * Fetch a JSON file, from a URL. Use a cache (of 1 hour) to avoid to many network request + * @param url + * @returns {*} + */ $scope.getJson = function(url) { return $q(function(resolve, reject) { $http.get(url, { @@ -165,6 +178,15 @@ function FeedController($scope, $timeout, $http, $translate, $q, csConfig, csHtt return (!item || (!item.title && !item.content_text && !item.content_html)); }; + /** + * Prepare a feed for the template : + * - set 'time' with a unix timestamp + * - set 'content' with the HTML or text content (truncated if too long) + * - fill authors if not exists, using feed authors + * @param item + * @param feed + * @returns {{content}|*} + */ $scope.prepareJsonFeedItem = function(item, feed) { if ($scope.isEmptyFeedItem(item)) throw Error('Empty feed item') @@ -197,15 +219,14 @@ function FeedController($scope, $timeout, $http, $translate, $q, csConfig, csHtt item.authors = item.authors || feed.authors; return item; - } - + }; /** * Prepare feed (e.g. clean duplicated title, when feed URL is a discourse topic, all item will have the same title) * @param feed * @returns {*} */ - $scope.prepareJsonFeed = function(feed) { + $scope.cleanDuplicatedTitles = function(feed) { if (!feed || !feed.items) return feed; _.forEach(feed.items, function(item, index) { @@ -216,12 +237,39 @@ function FeedController($scope, $timeout, $http, $translate, $q, csConfig, csHtt return feed; }; + /** + * Detect this the given JSON is from Discourse + * @param json + * @returns {*} + */ + $scope.isJsonDiscourse = function(json) { + return $scope.isDiscourseCategory(json) || $scope.isDiscourseTopic(json); + }; + + /** + * Transform a JSON from Discourse into JSON feed + * @param feedUrl + * @param json + * @returns {*} + */ + $scope.parseJsonDiscourse = function(feedUrl, json) { + // Detect if category category + if ($scope.isDiscourseCategory(json)) { + // Convert category to feed + return $scope.parseDiscourseCategory(feedUrl, json); + } + + // Convert topic to feed + return $scope.parseDiscourseTopic(feedUrl, json); + }; + $scope.isDiscourseCategory = function(category) { - return category && category.topic_list && Array.isArray(category.topic_list.topics); - } + return category && category.topic_list && Array.isArray(category.topic_list.topics) && + !!category.topic_list.more_topics_url || false; + }; $scope.parseDiscourseCategory = function(url, category, locale) { - // Make sure this is a valid topic + // Check is a discourse category if (!$scope.isDiscourseCategory(category)) throw new Error('Not a discourse category'); locale = locale || $translate.use(); @@ -242,28 +290,38 @@ function FeedController($scope, $timeout, $http, $translate, $q, csConfig, csHtt // Exclude category description (=tag 'about-category') if (topic.tags && topic.tags.includes('about-category')) return res; - // Skip if not expected language - var language = $scope.getLanguageFromTitle(topic.title); - if (!$scope.isCompatibleLanguage(locale, language)) return res; + // Detect language, from the title. Then skip if not compatible with expected locale + var topicLanguage = $scope.getLanguageFromTitle(topic.title); + if (!$scope.isCompatibleLanguage(locale, topicLanguage)) return res; + // Compute the URL to load the topic var topicUrl = [baseUrl, 't', topic.slug, topic.id].join('/') + '.json'; - return res.concat($scope.getJson(topicUrl)) - }, []) - ).then(function(topics) { - feed.items = topics.reduce(function(res, topic) { - if (!$scope.isDiscourseTopic(topic)) return res; // Not a topic: skip - var feedTopic = $scope.parseDiscourseTopic(baseUrl, topic, feed); - - if (!feedTopic.items || !feedTopic.items.length) return res; // Topic is empty: skip - return res.concat(feedTopic.items[0]); - }, []); + + // Load topic JSON + return res.concat($scope.getJson(topicUrl) + .catch(function(err) { + console.error("[feed] Failed to load discourse topic from '{}'".format(topicUrl), err); + return null; // continue + }) + ) + }, [])) + .then(function(topics) { + feed.items = topics.reduce(function(res, topic) { + if (!$scope.isDiscourseTopic(topic)) return res; // Not a topic: skip + + var feedTopic = $scope.parseDiscourseTopic(baseUrl, topic, feed); + + if (!feedTopic.items || !feedTopic.items.length) return res; // Topic is empty: skip + + return res.concat(feedTopic.items[0]); + }, []); return feed; }); - } + }; $scope.isDiscourseTopic = function(topic) { return topic && topic.title && topic.post_stream && Array.isArray(topic.post_stream.posts); - } + }; $scope.parseDiscourseTopic = function(url, topic, feed) { // Make sure this is a valid topic @@ -273,7 +331,8 @@ function FeedController($scope, $timeout, $http, $translate, $q, csConfig, csHtt var baseUrl = uri.protocol + '//' + uri.host + (uri.port != 443 && uri.port != 80 ? uri.port : ''); // Clean title (e.g. remove '(fr)' or '(en)' or '(es)') - var title = $scope.cleanTitle(topic.title); + // Prefer unicode title (e.g. emoji are replaced) + var title = $scope.cleanTitle(topic.unicode_title || topic.title); var language = $scope.getLanguageFromTitle(topic.title); // Prepare root feed, if not yet exists @@ -335,7 +394,7 @@ function FeedController($scope, $timeout, $http, $translate, $q, csConfig, csHtt $scope.cleanTitle = function(title) { if (!title) return undefined; return title.replace(/\s\([a-z]{2}(:?-[A-Z]{2})?\)$/, ''); - } + }; /** * Clean a title : remove locale string at the end (e.g. '(fr)' or '(en)' or '(es)') @@ -345,17 +404,17 @@ function FeedController($scope, $timeout, $http, $translate, $q, csConfig, csHtt if (!title) return undefined; var matches = /\s\(([a-z]{2}(:?-[A-Z]{2})?)\)$/.exec(title); return matches && matches[1]; - } + }; $scope.isCompatibleLanguage = function(expectedLocale, language) { if (!expectedLocale || !language || expectedLocale === language) return true; // Extract the language from the locale, then compare // E.g. 'fr-FR' => 'fr' - const expectedLanguage = expectedLocale.split('-', 2)[0]; + var expectedLanguage = expectedLocale.split('-', 2)[0]; return expectedLanguage.toLowerCase() === language.toLowerCase(); - } + }; csSettings.api.locale.on.changed($scope, function() { if ($scope.loading) return;