From 8e37b404a1451d9b73c368de1d209ba6b0d40cc6 Mon Sep 17 00:00:00 2001 From: Benoit Lavenier <benoit.lavenier@e-is.pro> Date: Thu, 10 Aug 2023 19:02:04 +0200 Subject: [PATCH] enh(home): Enable feeds to redirect to another json, using next_url --- app/config.json | 16 +- doc/feed/draft/feed-fr.json | 10 +- doc/feed/feed-en-GB.json | 9 + doc/feed/feed-en.json | 9 + doc/feed/feed-es-ES.json | 9 + doc/feed/feed-fr-FR.json | 7 +- scss/ionic.app.scss | 56 +++- www/index.html | 1 + www/js/config.js | 19 +- www/js/controllers.js | 30 +- www/js/controllers/feed-controllers.js | 365 +++++++++++++++++++++++++ www/js/controllers/home-controllers.js | 195 +------------ www/js/services/settings-services.js | 2 +- www/templates/feed/feed.html | 62 +++++ www/templates/home/home.html | 63 +---- 15 files changed, 575 insertions(+), 278 deletions(-) create mode 100644 doc/feed/feed-en-GB.json create mode 100644 doc/feed/feed-en.json create mode 100644 doc/feed/feed-es-ES.json create mode 100644 www/js/controllers/feed-controllers.js create mode 100644 www/templates/feed/feed.html diff --git a/app/config.json b/app/config.json index 0c14d7293..2cb8e7094 100644 --- a/app/config.json +++ b/app/config.json @@ -35,12 +35,20 @@ }, "feed": { "jsonFeed": { - "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" + "ca": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-ca.json", + "de-DE": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-de-DE.json", + "en": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-en.json", + "en-GB": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-en-GB.json", + "eo-EO": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-eo-EO.json", + "es-ES": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-es-ES.json", + "fr-FR": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-fr-FR.json", + "it-IT": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-it-IT.json", + "nl-NL": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-nl-NL.json", + "pt-PT": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-pt-PT.json" }, "maxContentLength": 1300, - "maxAgeInMonths": 3 + "maxAgeInMonths": 3, + "maxCount": 3 }, "fallbackNodes": [ { diff --git a/doc/feed/draft/feed-fr.json b/doc/feed/draft/feed-fr.json index 49f87cce0..0929a426b 100644 --- a/doc/feed/draft/feed-fr.json +++ b/doc/feed/draft/feed-fr.json @@ -1,23 +1,23 @@ { - "version": "https://jsonfeed.org/version/1", + "version": "https://jsonfeed.org/version/1.1", "user_comment": "Feed that use the jsonFeed format (see jsonFeed.org for details).", "title": "Actualités", "description": "Actualités de Cesium", "home_page_url": "https://forum.monnaie-libre.fr/tag/cesium", "feed_url": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-fr.json", - "author": { + "authors": [{ "name": "Benoit Lavenier", "url": "@BenoitLavenier", "avatar": "https://g1.data.duniter.fr/user/profile/38MEAZN68Pz1DTvT3tqgxx4yQP6snJCQhPqEFxbDk4aE/_image/avatar.png" - }, + }], "items": [ { "title": "Cesium évolue ! Aïe, ça va piquer mais…", - "author": { + "authors": [{ "name": "Elois", "url": "@elois", "avatar": "https://forum.monnaie-libre.fr/user_avatar/forum.monnaie-libre.fr/elois/45/185_2.png" - }, + }], "date_published": "2020-03-07T19:42:00+01:00", "id": "https://forum.monnaie-libre.fr/t/cesium-evolue-aie-ca-va-piquer-mais/10015", "url": "https://forum.monnaie-libre.fr/t/cesium-evolue-aie-ca-va-piquer-mais/10015", diff --git a/doc/feed/feed-en-GB.json b/doc/feed/feed-en-GB.json new file mode 100644 index 000000000..babed8914 --- /dev/null +++ b/doc/feed/feed-en-GB.json @@ -0,0 +1,9 @@ +{ + "version": "https://jsonfeed.org/version/1.1", + "title": "News", + "user_comment": "Ce fichier redirige vers le forum Duniter (via 'next_url')", + "feed_url": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-en-GB.json", + + "home_page_url": "https://forum.duniter.org/t/new-version-of-cesium-en/11460", + "next_url": "https://forum.duniter.org/t/new-version-of-cesium-en/11460.json" +} diff --git a/doc/feed/feed-en.json b/doc/feed/feed-en.json new file mode 100644 index 000000000..933843344 --- /dev/null +++ b/doc/feed/feed-en.json @@ -0,0 +1,9 @@ +{ + "version": "https://jsonfeed.org/version/1.1", + "title": "News", + "user_comment": "Ce fichier redirige vers le forum Duniter (via 'next_url')", + "feed_url": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-en.json", + + "home_page_url": "https://forum.duniter.org/t/new-version-of-cesium-en/11460", + "next_url": "https://forum.duniter.org/t/new-version-of-cesium-en/11460.json" +} diff --git a/doc/feed/feed-es-ES.json b/doc/feed/feed-es-ES.json new file mode 100644 index 000000000..8c6c2a7da --- /dev/null +++ b/doc/feed/feed-es-ES.json @@ -0,0 +1,9 @@ +{ + "version": "https://jsonfeed.org/version/1.1", + "title": "Noticias", + "user_comment": "Ce fichier redirige vers le forum Duniter (via 'next_url')", + "feed_url": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-es-ES.json", + + "home_page_url": "https://forum.duniter.org/t/nueva-version-de-cesium-es/11459", + "next_url": "https://forum.duniter.org/t/nueva-version-de-cesium-es/11459.json" +} diff --git a/doc/feed/feed-fr-FR.json b/doc/feed/feed-fr-FR.json index d6a2e32ec..469a14e30 100644 --- a/doc/feed/feed-fr-FR.json +++ b/doc/feed/feed-fr-FR.json @@ -1,10 +1,9 @@ { "version": "https://jsonfeed.org/version/1.1", "title": "Actualités", - "user_comment": "Ce fichier permet la redirection vers le forum Duniter, via 'next_url'", - - "home_page_url": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-fr-FR", + "user_comment": "Ce fichier redirige vers le forum Duniter (via 'next_url')", "feed_url": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-fr-FR.json", - "next_url": "https://forum.duniter.org/c/clients/cesium/22.json" + "home_page_url": "https://forum.duniter.org/t/nouvelle-version-de-cesium-fr/11458/2", + "next_url": "https://forum.duniter.org/t/nouvelle-version-de-cesium-fr/11458/2.json" } diff --git a/scss/ionic.app.scss b/scss/ionic.app.scss index 217a7abdf..aa39c4bfb 100644 --- a/scss/ionic.app.scss +++ b/scss/ionic.app.scss @@ -2756,6 +2756,19 @@ div[drop-zone]:hover { color: white !important; } + h1, .h1 { + font-size: 18pt; + } + h2, .h2 { + font-size: 16pt; + } + h3, .h3 { + font-size: 14pt; + } + h4, .h4 { + font-size: 12pt; + } + ul { list-style: unset; padding-left: 40px; @@ -2779,24 +2792,63 @@ div[drop-zone]:hover { border: 1px solid $positive; } .avatar-left-padding { - padding-left: 30px; + padding-inline-start: 30px; } } .feed-title, .card .title { margin-top: 5px; - font-size: 18pt; a { color: white !important;; } } + .tags { + font-size: small; + color: grey !important; + } + .feed-content, .card .content { text-align: start; color: lightgrey !important; + + img.emoji { + display: unset; + max-width: 12px; + max-height: 12px; + } + + blockquote p { + font-weight: unset; + font-size: unset; + line-height: unset; + } + + blockquote, .quote .title { + padding: 5px 10px; + border-left: 5px solid gray; + margin: 0 0 10px 0; + font-size: 10pt; + } + .quote .title { + margin: 0; + font-size: 10pt; + padding-left: 32px; + + .avatar { + position: absolute; + margin-top: -1px; + margin-left: -3px; + height: 20px; + width: 20px; + border: 1px solid $positive; + } + + } } + .feed-footer, .card .footer { a:hover { diff --git a/www/index.html b/www/index.html index 05b20173d..59b00cdad 100644 --- a/www/index.html +++ b/www/index.html @@ -160,6 +160,7 @@ <!-- controllers --> <script src="dist/dist_js/app/controllers/app-controllers.js"></script> <script src="dist/dist_js/app/controllers/home-controllers.js"></script> + <script src="dist/dist_js/app/controllers/feed-controllers.js"></script> <script src="dist/dist_js/app/controllers/join-controllers.js"></script> <script src="dist/dist_js/app/controllers/login-controllers.js"></script> <script src="dist/dist_js/app/controllers/help-controllers.js"></script> diff --git a/www/js/config.js b/www/js/config.js index 86f2af399..3885ccfd5 100644 --- a/www/js/config.js +++ b/www/js/config.js @@ -43,14 +43,21 @@ angular.module("cesium.config", []) "de-DE": "license/license_g1-de-DE" }, "feed": { - "jsonFeed": { - /*"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" + "jsonFeed": { + "ca": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-ca.json", + "de-DE": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-de-DE.json", + "en": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-en.json", + "en-GB": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-en-GB.json", + "eo-EO": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-eo-EO.json", + "es-ES": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-es-ES.json", + "fr-FR": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-fr-FR.json", + "it-IT": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-it-IT.json", + "nl-NL": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-nl-NL.json", + "pt-PT": "https://raw.githubusercontent.com/duniter/cesium/master/doc/feed/feed-pt-PT.json" }, "maxContentLength": 1300, - "maxAgeInMonths": 15 + "maxAgeInMonths": -1, + "maxCount": 3 }, "fallbackNodes": [ { diff --git a/www/js/controllers.js b/www/js/controllers.js index b36e1e613..b4812bce0 100644 --- a/www/js/controllers.js +++ b/www/js/controllers.js @@ -1,17 +1,17 @@ angular.module('cesium.controllers', [ - 'cesium.app.controllers', - 'cesium.home.controllers', - 'cesium.join.controllers', - 'cesium.login.controllers', - 'cesium.help.controllers', - 'cesium.wallet.controllers', - 'cesium.wallets.controllers', - 'cesium.currency.controllers', - 'cesium.wot.controllers', - 'cesium.transfer.controllers', - 'cesium.settings.controllers', - 'cesium.network.controllers', - 'cesium.blockchain.controllers' - ]) -; + 'cesium.app.controllers', + 'cesium.home.controllers', + 'cesium.feed.controllers', + 'cesium.join.controllers', + 'cesium.login.controllers', + 'cesium.help.controllers', + 'cesium.wallet.controllers', + 'cesium.wallets.controllers', + 'cesium.currency.controllers', + 'cesium.wot.controllers', + 'cesium.transfer.controllers', + 'cesium.settings.controllers', + 'cesium.network.controllers', + 'cesium.blockchain.controllers' +]); diff --git a/www/js/controllers/feed-controllers.js b/www/js/controllers/feed-controllers.js new file mode 100644 index 000000000..6907ff3b1 --- /dev/null +++ b/www/js/controllers/feed-controllers.js @@ -0,0 +1,365 @@ +angular.module('cesium.feed.controllers', ['cesium.services']) + + + .controller('FeedCtrl', FeedController) +; + +function FeedController($scope, $timeout, $http, $translate, $q, csConfig, csHttp, csCache, csSettings) { + 'ngInject'; + + $scope.search = { + loading: true, + maxCount: (csConfig.feed && csConfig.feed.maxCount) || 3, // 3 month by default + maxContentLength: (csConfig.feed && csConfig.feed.maxContentLength) || 1300, + maxAgeInMonths: (csConfig.feed && csConfig.feed.maxAgeInMonths) || 3, // 3 month by default + minTime: undefined, // unix time + loadCount: 0, + maxLoadCount: 2 // = 1 redirection max + }; + + $scope.enter = function(e, state) { + // Wait platform to be ready + csSettings.ready() + .then(function() { + return $scope.load(); + }) + .catch(function() { + // Continue + 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; + $scope.$parent.toggleFeed(showFeed); + }); + }; + + $scope.$on('$ionicParentView.enter', $scope.enter); + + $scope.load = function() { + var feedUrl = csSettings.getFeedUrl(); + if (!feedUrl || typeof feedUrl !== 'string') return; // Skip + + // Min unix time, to exclude old topics + var maxAgeInMonths = $scope.search.maxAgeInMonths; + var minDate = maxAgeInMonths > 0 ? moment().subtract(maxAgeInMonths, 'month').startOf('day').utc() : undefined; + $scope.search.minTime = minDate && minDate.unix(); + + var now = Date.now(); + console.debug("[feed] Loading from {url: {0}, minTime: '{1}'}".format(feedUrl, minDate && minDate.toISOString() || 'all')); + + return $scope.getJsonFeed(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 + + return feed; + }) + .catch(function(err) { + console.error('[feed] Failed to load.', err); + return null; + }); + }; + + $scope.getJsonFeed = function(feedUrl, maxCount) { + var locale = $translate.use(); + maxCount = maxCount || $scope.search.maxCount; + var minTime = $scope.search.minTime; + + $scope.search.loadCount++; + return $scope.getJson(feedUrl) + .then(function(json) { + + // Detect Discourse category, then convert + if ($scope.isDiscourseCategory(json)) { + return $scope.parseDiscourseCategory(feedUrl, json); + } + + // Detect Discourse topic, then convert + else if ($scope.isDiscourseTopic(json)) { + return $scope.parseDiscourseTopic(feedUrl, json); + } + + return json; + }) + .then(function(feed) { + if (!feed || !feed.items && !feed.next_url) return null; // skip + + // SKip if incompatible language + if (feed.language && !$scope.isCompatibleLanguage(locale, feed.language)) { + console.debug("[feed] Skip feed item '{0}' - Expected language: '{1}', actual: '{2}'".format(feed.title, locale, feed.language)); + return null; + } + + feed.items = (feed.items || []).reduce(function (res, item) { + + // Skip if empty (missing title and content) + if ($scope.isEmptyFeedItem(item)) return res; + + item = $scope.prepareJsonFeedItem(item, feed); + + // Skip if too old items + if (minTime > 0 && item.creationTime && (item.creationTime < minTime)) return res; + + // Skip if not same language + if (item.language && !$scope.isCompatibleLanguage(locale, item.language)) { + console.debug("[feed] Feed item '{0}' EXCLUDED - expected locale: {1}, actual language: {2}".format(item.title || feed.title, locale, item.language)); + return res; + } + + return res.concat(item); + }, []); + + return feed; + }) + .then(function(feed) { + if (!feed) return null; // skip + 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); + 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) { + + console.debug("[feed] Loading from {next_url: '{0}'}".format(feed.next_url)); + + // Fetch more + return $scope.getJsonFeed(feed.next_url, maxCount - 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)); + } + + return feed; + }); + } + + return feed; + }); + } + + $scope.getJson = function(url) { + return $q(function(resolve, reject) { + $http.get(url, { + timeout: csConfig.timeout, + responseType: 'json', + cache: csCache.get('csFeed-', csCache.constants.LONG) + }) + .success(resolve) + .error(reject) + }); + }; + + $scope.isEmptyFeedItem = function(item) { + return (!item || (!item.title && !item.content_text && !item.content_html)); + }; + + $scope.prepareJsonFeedItem = function(item, feed) { + if ($scope.isEmptyFeedItem(item)) throw Error('Empty feed item') + + var maxContentLength = $scope.search.maxContentLength; + + // Convert UTC time + if (item.date_published) { + item.creationTime = moment.utc(item.date_published).unix(); + } + if (item.date_modified) { + item.time = moment.utc(item.date_modified).unix(); + } + + // Convert content to HTML + if (item.content_html) { + item.content = item.content_html; + } + else { + item.content = (item.content_text||'').replace(/\n/g, '<br/>'); + } + + // Trunc content, if need + if (maxContentLength > 0 && item.content && item.content.length > maxContentLength) { + var endIndex = Math.max(item.content.lastIndexOf(" ", maxContentLength), item.content.lastIndexOf("<", maxContentLength)); + item.content = item.content.substring(0, endIndex) + ' (...)'; + item.truncated = true; + } + + // If author is missing, copy the main author + 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) { + if (!feed || !feed.items) return feed; + + _.forEach(feed.items, function(item, index) { + if (item.title && index > 0 && (item.title === feed.items[0].title)) { + delete item.title; + } + }); + return feed; + }; + + $scope.isDiscourseCategory = function(category) { + return category && category.topic_list && Array.isArray(category.topic_list.topics); + } + + $scope.parseDiscourseCategory = function(url, category, locale) { + // Make sure this is a valid topic + if (!$scope.isDiscourseCategory(category)) throw new Error('Not a discourse category'); + + locale = locale || $translate.use(); + var uri = csHttp.uri.parse(url); + var baseUrl = uri.protocol + '//' + uri.host + (uri.port != 443 && uri.port != 80 ? uri.port : ''); + var pageUrl = baseUrl + category.topic_list.more_topics_url.replace(/\?page=[0-9]+/, ''); + var feed = { + version: "https://jsonfeed.org/version/1.1", // fixed value + home_page_url: pageUrl, + feed_url: url, + title: 'HOME.FEEDS_TITLE' + }; + + return $q.all( + category.topic_list.topics.reduce(function(res, topic) { + if (!topic.pinned || !topic.visible) return res; // Skip not pinned topic + + // 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; + + 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 : ''); + + // Clean title (e.g. remove '(fr)' or '(en)' or '(es)') + var title = $scope.cleanTitle(topic.title); + var language = $scope.getLanguageFromTitle(topic.title); + + // Prepare root feed, if not yet exists + feed = feed || { + version: "https://jsonfeed.org/version/1.1", // fixed value + home_page_url: [baseUrl, 't', topic.slug, topic.id].join('/'), + feed_url: url, + title: 'HOME.FEEDS_TITLE', + language: language + }; + feed.language = feed.language || language; + + feed.items = topic.post_stream.posts.reduce(function(res, post) { + if (!post.cooked || post.cooked.trim() === '') return res; // Skip if empty + + // SKip if hidden, or deleted post + if (post.hidden || post.deleted_at) return res; + + 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; + } + + // Fill parent feed defaults + feed.authors = feed.authors || [author]; + + return res.concat({ + id: post.id, + url: [baseUrl, 't', post.topic_slug, post.topic_id, post.post_number].join('/'), + title: title, + date_published: post.created_at, + date_modified: post.updated_at, + content_html: post.cooked, + authors: [author], + language: language, + tags: post.tags || topic.tags + }); + }, []); + + return feed; + }; + + /** + * Clean a title : remove locale string at the end (e.g. '(fr)' or '(en)' or '(es)') + * @param title + */ + $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)') + * @param title + */ + $scope.getLanguageFromTitle = function(title) { + 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]; + + return expectedLanguage.toLowerCase() === language.toLowerCase(); + } + + csSettings.api.locale.on.changed($scope, function() { + if ($scope.loading) return; + console.debug("[feed] Locale changed. Reload feed..."); + $scope.enter(); + }); +} diff --git a/www/js/controllers/home-controllers.js b/www/js/controllers/home-controllers.js index 27529ebc0..981a87c26 100644 --- a/www/js/controllers/home-controllers.js +++ b/www/js/controllers/home-controllers.js @@ -33,6 +33,7 @@ function HomeController($scope, $state, $timeout, $ionicHistory, $translate, $ht $scope.locales = angular.copy(csSettings.locales); $scope.smallscreen = UIUtils.screen.isSmall(); $scope.showInstallHelp = false; + $scope.showFeed = false; $scope.enter = function(e, state) { if (ionic.Platform.isIOS() && window.StatusBar) { @@ -59,15 +60,12 @@ function HomeController($scope, $state, $timeout, $ionicHistory, $translate, $ht // Wait platform to be ready csPlatform.ready() - .then(function() { - $scope.loading = false; - $scope.loadingMessage = ''; - $scope.loadFeeds(); - }) .catch(function(err) { $scope.node = csCurrency.data.node; - $scope.loading = false; $scope.error = err; + }) + .then(function() { + $scope.loading = false; $scope.loadingMessage = ''; }); } @@ -81,185 +79,6 @@ function HomeController($scope, $state, $timeout, $ionicHistory, $translate, $ht $timeout($scope.enter, 200); }; - $scope.loadFeeds = function() { - var feedUrl = csSettings.getFeedUrl(); - 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 recent feeds at {url: {0}, minTime: '{1}'}".format(feedUrl, minDate.toISOString())); - - $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 - - // Convert UTC time - if (item.date_published) { - 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; - } - else { - item.content = (item.content_text||'').replace(/\n/g, '<br/>'); - } - - // 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.substring(0, endIndex) + ' (...)'; - item.truncated = true; - } - - // If author is missing, copy the main author - item.author = item.author || feed.author; - - return res.concat(item); - }, []); - - if (!feed.items.length) return; // No items: skip - - $scope.feed = feed; - }) - .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 @@ -278,9 +97,13 @@ function HomeController($scope, $state, $timeout, $ionicHistory, $translate, $ht $scope.hideLocalesPopover(); csSettings.data.locale = _.findWhere($scope.locales, {id: langKey}); csSettings.store(); - $scope.loadFeeds(); }; + $scope.toggleFeed = function(show) { + $scope.showFeed = (show !== undefined) ? show : !$scope.showFeed + $scope.$broadcast('$$rebind::feed'); // force rebind feed + } + /* -- show/hide locales popup -- */ $scope.showLocalesPopover = function(event) { diff --git a/www/js/services/settings-services.js b/www/js/services/settings-services.js index 84594f2f4..7a37c0de4 100644 --- a/www/js/services/settings-services.js +++ b/www/js/services/settings-services.js @@ -308,7 +308,7 @@ angular.module('cesium.settings.services', ['ngApi', 'cesium.config']) function getFeedUrl() { var locale = data.locale && data.locale.id || csConfig.defaultLanguage || 'en'; return (csConfig.feed && csConfig.feed.jsonFeed) ? - (csConfig.feed.jsonFeed[locale] ? csConfig.feed.jsonFeed[locale] : defaultSettings.feed.jsonFeed[csConfig.defaultLanguage || 'en'] || csConfig.feed) : undefined; + (csConfig.feed.jsonFeed[locale] ? csConfig.feed.jsonFeed[locale] : defaultSettings.feed.jsonFeed[csConfig.defaultLanguage || 'en'] || csConfig.feed.jsonFeed) : undefined; } // Detect locale successful changes, then apply to vendor libs diff --git a/www/templates/feed/feed.html b/www/templates/feed/feed.html new file mode 100644 index 000000000..ec30b864e --- /dev/null +++ b/www/templates/feed/feed.html @@ -0,0 +1,62 @@ + + <!-- feed --> + <div class="feed padding-horizontal no-padding-xs padding-top" + ng-controller="FeedCtrl"> + <h3 class="padding-left"> + <i class="icon ion-speakerphone"></i> + {{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> + </a></small> + </h3> + + <!-- card for each item --> + <div ng-repeat="item in feed.items" class="card padding no-margin-xs"> + + <div class="header"> + + <!-- authors --> + <ng-repeat ng-repeat="author in item.authors track by author.name"> + <i ng-if="author.avatar" class="avatar" style="background-image: url({{author.avatar}});"></i> + <a ng-class="{'avatar-left-padding': author.avatar}" class="author" + ng-click="author.url && openLink($event, author.url)"> + {{author.name}} + </a> + </ng-repeat> + + <!-- time --> + <a ng-if="item.time" + title="{{item.time|formatDate}}" + ng-click="openLink($event, item.url)" + class="item-note "> + <small><i class="icon ion-clock"></i> {{item.creationTime|formatFromNow}}</small> + </a> + </div> + + <!-- title --> + <h2 class="title feed-title"> + <a ng-click="openLink($event, item.url)">{{item.title}}</a> + + <!-- tags --> + <div class="tags" ng-if="item.tags && item.tags.length"> + <span ng-repeat="tag in item.tags">#{{tag}}</span> + </div> + </h2> + + + <!-- content --> + <div ng-if="item.content" + class="content feed-content" + trust-as-html="item.content"></div> + + <!-- footer --> + <h4 class="card-footer feed-footer text-right positive-100"> + <a ng-click="openLink($event, item.url)"> + <span ng-if="item.truncated" translate>HOME.READ_MORE</span> + <span ng-if="!item.truncated" translate>COMMON.BTN_SHOW</span> + <i class="icon ion-chevron-right"></i> + </a> + </h4> + </div> +</div> diff --git a/www/templates/home/home.html b/www/templates/home/home.html index c49100cad..0b98de007 100644 --- a/www/templates/home/home.html +++ b/www/templates/home/home.html @@ -1,4 +1,4 @@ -<ion-view id="home" bind-notifier="{locale:$root.settings.locale.id}"> +<ion-view id="home" bind-notifier="{locale:$root.settings.locale.id, feed: showFeed}"> <!-- no title --> <ion-nav-title></ion-nav-title> @@ -156,62 +156,15 @@ </div> - </div> - <div class="col no-padding" ng-class="{'col-30': !feed, 'col-10': feed}"> + + <!-- left col spacer--> + <div class="col no-padding" ng-class=":feed:{'col-30': !showFeed, 'col-10': showFeed}"> </div> - <div class="col col-30 no-padding" ng-if="feed"> - - <!-- feed --> - <div class="feed padding-horizontal no-padding-xs padding-top"> - <h3 class="padding-left"> - <i class="icon ion-speakerphone"></i> - {{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> - </a></small> - </h3> - - <!-- feed items --> - <div class="animate-show-hide ng-hide" ng-show="feed"> - <div ng-repeat="item in feed.items" - class="card padding no-margin-xs"> - - <div class="header "> - <!-- author --> - <i ng-if="item.author.avatar" class="avatar" style="background-image: url({{item.author.avatar}});"></i> - <a ng-class="{'avatar-left-padding': item.author.avatar}" class="author" - ng-click="item.author.url && openLink($event, item.author.url)"> - {{item.author.name}} - </a> - - <!-- time --> - <a ng-if="item.time" - title="{{item.time|formatDate}}" - ng-click="openLink($event, item.url)" - class="item-note "> - <small><i class="icon ion-clock"></i> {{item.creationTime|formatFromNow}}</small> - </a> - </div> - <!-- title --> - <h2 class="title feed-title"> - <a ng-click="openLink($event, item.url)">{{item.title}}</a></h2> - <div ng-if="item.content" - class="content feed-content" - trust-as-html="item.content"></div> - - <!-- footer --> - <h4 class="card-footer feed-footer text-right positive-100"> - <a ng-click="openLink($event, item.url)"> - <span ng-if="item.truncated" translate>HOME.READ_MORE</span> - <span ng-if="!item.truncated" translate>COMMON.BTN_SHOW</span> - <i class="icon ion-chevron-right"></i> - </a> - </h4> - </div> - </div> - </div> + + <!-- include feeds --> + <div class="col col-30 no-padding animate-show-hide ng-hide" ng-show=":feed:showFeed"> + <ng-include src="::'templates/feed/feed.html'"></ng-include> </div> </div> -- GitLab