From 76b299e03f7d885b406f37b7cce213d0e278c8ad Mon Sep 17 00:00:00 2001 From: Benoit Lavenier <benoit.lavenier@e-is.pro> Date: Wed, 9 Aug 2023 20:40:13 +0200 Subject: [PATCH] enh(home): Parse feeds from a discourse topic or category URL --- app/config.json | 7 +- www/i18n/locale-ca.json | 1 + www/i18n/locale-de-DE.json | 3 +- www/i18n/locale-en-GB.json | 3 +- www/i18n/locale-en.json | 3 +- www/i18n/locale-eo-EO.json | 3 +- www/i18n/locale-es-ES.json | 1 + www/i18n/locale-fr-FR.json | 3 +- www/i18n/locale-it-IT.json | 3 +- www/i18n/locale-nl-NL.json | 3 +- www/i18n/locale-pt-PT.json | 1 + www/js/config.js | 13 +-- www/js/controllers/home-controllers.js | 151 +++++++++++++++++++++++-- www/templates/home/home.html | 4 +- 14 files changed, 168 insertions(+), 31 deletions(-) diff --git a/app/config.json b/app/config.json index b75a20841..0c14d7293 100644 --- a/app/config.json +++ b/app/config.json @@ -35,11 +35,12 @@ }, "feed": { "jsonFeed": { - "fr-FR": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-fr.json", - "en": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-en.json", + "fr-FR": "https://forum.monnaie-libre.fr/c/tools/cesium/110.json", + "en": "https://forum.monnaie-libre.fr/t/actualites-fr/28088.json", "es": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-es.json" }, - "maxContentLength": 1300 + "maxContentLength": 1300, + "maxAgeInMonths": 3 }, "fallbackNodes": [ { diff --git a/www/i18n/locale-ca.json b/www/i18n/locale-ca.json index 7db48c3f5..a93f3cb02 100644 --- a/www/i18n/locale-ca.json +++ b/www/i18n/locale-ca.json @@ -117,6 +117,7 @@ }, "HOME": { "FEED_SOURCE": "Font", + "FEEDS_TITLE": "NotÃcies", "READ_MORE": "Amplia", "SHOW_ALL_FEED": "Mostra-m'ho tot", "TITLE": "Cesium", diff --git a/www/i18n/locale-de-DE.json b/www/i18n/locale-de-DE.json index 98a491564..dca7b5644 100644 --- a/www/i18n/locale-de-DE.json +++ b/www/i18n/locale-de-DE.json @@ -139,7 +139,8 @@ "NETWORK_CONNECTION_ERROR": "Netzwerk ist nicht erreichbar.<br/><br/>Überprüfen Sie Ihre Internetverbindung, oder wählen Sie einen Knoten manuell <a class=\"positive\" ng-click=\"doQuickFix('settings')\">in den Einstellungen</a> aus.", "SHOW_ALL_FEED": "Alles anzeigen", "READ_MORE": "Mehr lesen", - "FEED_SOURCE": "Quelle" + "FEED_SOURCE": "Quelle", + "FEEDS_TITLE": "Nachrichten" }, "SETTINGS": { "TITLE": "Einstellungen", diff --git a/www/i18n/locale-en-GB.json b/www/i18n/locale-en-GB.json index 893ff2a9d..97ae039e5 100644 --- a/www/i18n/locale-en-GB.json +++ b/www/i18n/locale-en-GB.json @@ -135,7 +135,8 @@ "NETWORK_CONNECTION_ERROR": "Network is unreachable.<br/><br/>Check your Internet connection, or select a peer manually <a class=\"positive\" ng-click=\"doQuickFix('settings')\">in the settings</a>.", "SHOW_ALL_FEED": "Show all", "READ_MORE": "Read more", - "FEED_SOURCE": "Source" + "FEED_SOURCE": "Source", + "FEEDS_TITLE": "News" }, "SETTINGS": { "TITLE": "Settings", diff --git a/www/i18n/locale-en.json b/www/i18n/locale-en.json index 817b9c948..a23d15730 100644 --- a/www/i18n/locale-en.json +++ b/www/i18n/locale-en.json @@ -135,7 +135,8 @@ "NETWORK_CONNECTION_ERROR": "Network is unreachable.<br/><br/>Check your Internet connection, or select a peer manually <a class=\"positive\" ng-click=\"doQuickFix('settings')\">in the settings</a>.", "SHOW_ALL_FEED": "Show all", "READ_MORE": "Read more", - "FEED_SOURCE": "Source" + "FEED_SOURCE": "Source", + "FEEDS_TITLE": "News" }, "SETTINGS": { "TITLE": "Settings", diff --git a/www/i18n/locale-eo-EO.json b/www/i18n/locale-eo-EO.json index fafe943f8..b355afdff 100644 --- a/www/i18n/locale-eo-EO.json +++ b/www/i18n/locale-eo-EO.json @@ -135,7 +135,8 @@ "NETWORK_CONNECTION_ERROR": "Reto neatingeblas.<br/><br/>Kontrolu vian interreta konekto, aÅ selektu nodon permane <a class=\"positive\" ng-click=\"doQuickFix('settings')\">en la agordoj</a>.", "SHOW_ALL_FEED": "Vidi ĉion", "READ_MORE": "Legi la sekvon", - "FEED_SOURCE": "Fonto" + "FEED_SOURCE": "Fonto", + "FEEDS_TITLE": "Novaĵoj" }, "SETTINGS": { "TITLE": "Parametroj", diff --git a/www/i18n/locale-es-ES.json b/www/i18n/locale-es-ES.json index 8021895d0..f60c11f2e 100644 --- a/www/i18n/locale-es-ES.json +++ b/www/i18n/locale-es-ES.json @@ -117,6 +117,7 @@ }, "HOME": { "FEED_SOURCE": "Fuente", + "FEEDS_TITLE": "Noticias", "READ_MORE": "Leer más", "SHOW_ALL_FEED": "Ver todo", "TITLE": "Cesium", diff --git a/www/i18n/locale-fr-FR.json b/www/i18n/locale-fr-FR.json index a10c5e26d..121321c68 100644 --- a/www/i18n/locale-fr-FR.json +++ b/www/i18n/locale-fr-FR.json @@ -135,7 +135,8 @@ "NETWORK_CONNECTION_ERROR": "Réseau injoignable.<br/><br/>Vérifiez votre connexion Internet, ou sélectionnez un nÅ“ud manuellement <a class=\"positive\" ng-click=\"doQuickFix('settings')\">dans les paramètres</a>.", "SHOW_ALL_FEED": "Voir tout", "READ_MORE": "Lire la suite", - "FEED_SOURCE": "Source" + "FEED_SOURCE": "Source", + "FEEDS_TITLE": "Actualités" }, "SETTINGS": { "TITLE": "Paramètres", diff --git a/www/i18n/locale-it-IT.json b/www/i18n/locale-it-IT.json index c8739f03e..b21269c1f 100644 --- a/www/i18n/locale-it-IT.json +++ b/www/i18n/locale-it-IT.json @@ -135,7 +135,8 @@ "NETWORK_CONNECTION_ERROR": "Rete non raggiungibile.<br/><br/>Controlla la tua connessione Internet, o seleziona un nodo manualmente <a class=\"positive\" ng-click=\"doQuickFix('settings')\">nelle impostazioni</a>.", "SHOW_ALL_FEED": "Mostra tutto", "READ_MORE": "Leggi di più", - "FEED_SOURCE": "Fonte" + "FEED_SOURCE": "Fonte", + "FEEDS_TITLE": "Notizie" }, "SETTINGS": { "TITLE": "Impostazioni", diff --git a/www/i18n/locale-nl-NL.json b/www/i18n/locale-nl-NL.json index 716698e5a..f04ac3aab 100644 --- a/www/i18n/locale-nl-NL.json +++ b/www/i18n/locale-nl-NL.json @@ -126,7 +126,8 @@ "NETWORK_CONNECTION_ERROR": "Netwerk is niet bereikbaar.<br/><br/>Controleer uw internetverbinding, of selecteer een node handmatig <a class=\"positive\" ng-click=\"doQuickFix('settings')\">in de instellingen</a>.", "SHOW_ALL_FEED": "Alles weergeven", "READ_MORE": "Lees meer", - "FEED_SOURCE": "Bron" + "FEED_SOURCE": "Bron", + "FEEDS_TITLE": "Nieuws" }, "SETTINGS": { "TITLE": "Instellingen", diff --git a/www/i18n/locale-pt-PT.json b/www/i18n/locale-pt-PT.json index 40c50ecc7..b7fc19e12 100644 --- a/www/i18n/locale-pt-PT.json +++ b/www/i18n/locale-pt-PT.json @@ -117,6 +117,7 @@ }, "HOME": { "FEED_SOURCE": "Fonte", + "FEEDS_TITLE": "NotÃcias", "READ_MORE": "Ler mais", "SHOW_ALL_FEED": "Ver tudo", "TITLE": "Cesium", diff --git a/www/js/config.js b/www/js/config.js index 0364af5a1..86f2af399 100644 --- a/www/js/config.js +++ b/www/js/config.js @@ -44,21 +44,18 @@ angular.module("cesium.config", []) }, "feed": { "jsonFeed": { - "fr-FR": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-fr.json", + /*"fr-FR": "https://forum.monnaie-libre.fr/t/actualites-test/28088/2.json",*/ + "fr-FR": "https://forum.monnaie-libre.fr/c/tools/cesium/110.json", "en": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-en.json", "es": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-es.json" }, - "maxContentLength": 1300 + "maxContentLength": 1300, + "maxAgeInMonths": 15 }, "fallbackNodes": [ { - "host": "g1v1.p2p.legal", + "host": "g1.e-is.pro", "port": 443 - }, - { - "host": "duniter.moul.re", - "port": 443, - "path": "/bma" } ], "developers": [ diff --git a/www/js/controllers/home-controllers.js b/www/js/controllers/home-controllers.js index 77326e564..27529ebc0 100644 --- a/www/js/controllers/home-controllers.js +++ b/www/js/controllers/home-controllers.js @@ -25,7 +25,7 @@ angular.module('cesium.home.controllers', ['cesium.platform', 'cesium.services'] ; function HomeController($scope, $state, $timeout, $ionicHistory, $translate, $http, $q, $location, - UIUtils, BMA, Device, csConfig, csCache, csPlatform, csCurrency, csSettings) { + UIUtils, BMA, Device, csConfig, csHttp, csCache, csPlatform, csCurrency, csSettings) { 'ngInject'; $scope.loading = true; @@ -86,22 +86,47 @@ function HomeController($scope, $state, $timeout, $ionicHistory, $translate, $ht if (!feedUrl || typeof feedUrl !== 'string') return; // Skip var maxContentLength = (csConfig.feed && csConfig.feed.maxContentLength) || 650; + var maxAgeInMonths = (csConfig.feed && csConfig.feed.maxAgeInMonths) || 3; // 3 month by default + // Min unix time, to exclude old topics + var minDate = moment().subtract(maxAgeInMonths, 'month').utc(); + var minTime = minDate.unix(); var now = Date.now(); - console.debug("[home] Loading feeds from {0}...".format(feedUrl)); + console.debug("[home] Loading recent feeds at {url: {0}, minTime: '{1}'}".format(feedUrl, minDate.toISOString())); - $http.get(feedUrl, {responseType: 'json', cache: csCache.get(null, csCache.constants.LONG)}) - .success(function(feed) { - console.debug('[home] Feeds loaded in {0}ms'.format(Date.now()-now)); - if (!feed || !feed.items || !feed.items.length) return; // skip if empty + $scope.getJson(feedUrl) + .then(function(feed) { + console.debug('[home] Feeds loaded in {0}ms'.format(Date.now() - now)); + + // Detect Discourse category, then convert + if ($scope.isDiscourseCategory(feed)) { + return $scope.parseDiscourseCategory(feedUrl, feed); + } + + // Detect Discourse topic, then convert + else if ($scope.isDiscourseTopic(feed)) { + return $scope.parseDiscourseTopic(feedUrl, feed); + } + + return feed; + }) + .then(function(feed) { + if (!feed || !feed.items) return; // skip feed.items = feed.items.reduce(function(res, item) { - if (!item || (!item.title && !item.content_text && !item.content_html)) return res; // Skip + if (!item || (!item.title && !item.content_text && !item.content_html)) return res; // Skip // Convert UTC time if (item.date_published) { - item.time = moment.utc(item.date_published).unix(); + item.creationTime = moment.utc(item.date_published).unix(); } + if (item.date_modified) { + item.time = moment.utc(item.date_modified).unix(); + } + + // Skip if too old items + if (item.creationTime && (item.creationTime < minTime)) return res; + // Convert content to HTML if (item.content_html) { item.content = item.content_html; @@ -113,7 +138,7 @@ function HomeController($scope, $state, $timeout, $ionicHistory, $translate, $ht // Trunc content, if need if (maxContentLength !== -1 && item.content && item.content.length > maxContentLength) { var endIndex = Math.max(item.content.lastIndexOf(" ", maxContentLength), item.content.lastIndexOf("<", maxContentLength)); - item.content = item.content.substr(0, endIndex) + ' (...)'; + item.content = item.content.substring(0, endIndex) + ' (...)'; item.truncated = true; } @@ -123,14 +148,118 @@ function HomeController($scope, $state, $timeout, $ionicHistory, $translate, $ht return res.concat(item); }, []); + if (!feed.items.length) return; // No items: skip + $scope.feed = feed; }) - .error(function(data, status) { - console.error('[home] Failed to load feeds.'); + .catch(function(err) { + console.error('[home] Failed to load feeds.', err); $scope.feed = null; }); }; + $scope.getJson = function(url) { + return $q(function(resolve, reject) { + $http.get(url, { + timeout: csConfig.timeout, + responseType: 'json', + cache: csCache.get(null, csCache.constants.LONG) + }) + .success(resolve) + .error(reject) + }); + }; + + $scope.isDiscourseCategory = function(category) { + return category && category.topic_list && Array.isArray(category.topic_list.topics); + } + + $scope.parseDiscourseCategory = function(url, category) { + // Make sure this is a valid topic + if (!$scope.isDiscourseCategory(category)) throw new Error('Not a discourse category'); + + var uri = csHttp.uri.parse(url); + var baseUrl = uri.protocol + '//' + uri.host + (uri.port != 443 && uri.port != 80 ? uri.port : ''); + var feed = { + version: "https://jsonfeed.org/version/1", // fixed value + home_page_url: category.topic_list.more_topics_url.replace(/\?page=[0-9]+/, ''), + feed_url: url, + title: 'HOME.FEEDS_TITLE' // FIXME: how get the category title ? + }; + + return $q.all( + category.topic_list.topics.reduce(function(res, topic) { + if (!topic.pinned) return res; // Skip not pinned 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]); + }, []); + 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 + if (!$scope.isDiscourseTopic(topic)) throw new Error('Not a discourse topic'); + + var uri = csHttp.uri.parse(url); + var baseUrl = uri.protocol + '//' + uri.host + (uri.port != 443 && uri.port != 80 ? uri.port : ''); + + // Prepare root feed, if not yet exists + feed = feed || { + version: "https://jsonfeed.org/version/1", // fixed value + home_page_url: [baseUrl, 't', topic.slug, topic.id].join('/'), + feed_url: url, + title: topic.title + }; + + feed.items = topic.post_stream.posts.reduce(function(res, post) { + if (!post.cooked || post.cooked.trim() === '') return res; // Skip if empty + + var author = { + name: post.display_username, + url: [baseUrl, 'u', post.username].join('/'), + avatar: post.avatar_template ? (baseUrl + post.avatar_template.replace('{size}', '60')) : undefined + } + + // Try to resolve author pubkey, to replace author url by a link to wot identity + var developer = _.find(csConfig.developers || [], function(developer) { + return developer.name && ( + (post.display_username && developer.name.toLowerCase() === post.display_username.toLowerCase()) || + (post.username && developer.name.toLowerCase() === post.username.toLowerCase()) + ); + }); + if (developer && developer.pubkey) { + author.url = '@' + developer.pubkey; + } + + return res.concat({ + id: post.id, + url: [baseUrl, 't', post.topic_slug, post.topic_id, post.post_number].join('/'), + title: feed.title === topic.title ? '' : topic.title, // Only if different + date_published: post.created_at, + date_modified: post.updated_at, + content_html: post.cooked, + author: author, + tags: post.tags || topic.tags + }); + }, []); + + return feed; + }; + /** * Catch click for quick fix * @param action diff --git a/www/templates/home/home.html b/www/templates/home/home.html index 6e1473387..c49100cad 100644 --- a/www/templates/home/home.html +++ b/www/templates/home/home.html @@ -166,7 +166,7 @@ <div class="feed padding-horizontal no-padding-xs padding-top"> <h3 class="padding-left"> <i class="icon ion-speakerphone"></i> - {{feed.title}} + {{feed.title|translate}} <small><a ng-click="openLink($event, feed.home_page_url)" class="gray"> <span translate>HOME.SHOW_ALL_FEED</span> <i class="icon ion-chevron-right"></i> @@ -191,7 +191,7 @@ title="{{item.time|formatDate}}" ng-click="openLink($event, item.url)" class="item-note "> - <small><i class="icon ion-clock"></i> {{item.time|formatFromNow}}</small> + <small><i class="icon ion-clock"></i> {{item.creationTime|formatFromNow}}</small> </a> </div> <!-- title --> -- GitLab