Browse Source

render article on the client using headlines data

Andrew Dolgov 1 year ago
parent
commit
bd66a9ef28
8 changed files with 128 additions and 149 deletions
  1. 4 1
      classes/article.php
  2. 38 40
      classes/feeds.php
  3. 53 46
      js/Article.js
  4. 0 29
      js/ArticleCache.js
  5. 20 26
      js/Headlines.js
  6. 13 0
      js/common.js
  7. 0 5
      js/tt-rss.js
  8. 0 2
      plugins/note/note.js

+ 4 - 1
classes/article.php

@@ -27,6 +27,7 @@ class Article extends Handler_Protected {
 		}
 	}
 
+	/*
 	function view() {
 		$id = clean($_REQUEST["id"]);
 		$cids = explode(",", clean($_REQUEST["cids"]));
@@ -63,8 +64,9 @@ class Article extends Handler_Protected {
 		}
 
 		print json_encode($articles);
-	}
+	} */
 
+	/*
 	private function catchupArticleById($id, $cmode) {
 
 		if ($cmode == 0) {
@@ -86,6 +88,7 @@ class Article extends Handler_Protected {
 		$feed_id = $this->getArticleFeed($id);
 		CCache::update($feed_id, $_SESSION["uid"]);
 	}
+	*/
 
 	static function create_published_article($title, $url, $content, $labels_str,
 			$owner_uid) {

+ 38 - 40
classes/feeds.php

@@ -285,60 +285,58 @@ class Feeds extends Handler_Protected {
 
 				if (!$line["feed_title"]) $line["feed_title"] = "";
 
-				if (get_pref('COMBINED_DISPLAY_MODE')) {
-
-					$line["buttons_left"] = "";
-					foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_LEFT_BUTTON) as $p) {
-						$line["buttons_left"] .= $p->hook_article_left_button($line);
-					}
+                $line["buttons_left"] = "";
+                foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_LEFT_BUTTON) as $p) {
+                    $line["buttons_left"] .= $p->hook_article_left_button($line);
+                }
 
-					$line["buttons"] = "";
-					foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_BUTTON) as $p) {
-						$line["buttons"] .= $p->hook_article_button($line);
-					}
+                $line["buttons"] = "";
+                foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_BUTTON) as $p) {
+                    $line["buttons"] .= $p->hook_article_button($line);
+                }
 
-					$line["content"] = sanitize($line["content"],
-						$line['hide_images'], false, $line["site_url"], $highlight_words, $line["id"]);
+                $line["content"] = sanitize($line["content"],
+                    $line['hide_images'], false, $line["site_url"], $highlight_words, $line["id"]);
 
-					foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ARTICLE_CDM) as $p) {
-						$line = $p->hook_render_article_cdm($line);
-					}
+                foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ARTICLE_CDM) as $p) {
+                    $line = $p->hook_render_article_cdm($line);
+                }
 
-					$line['content'] = rewrite_cached_urls($line['content']);
-					$line["content"] = htmlspecialchars($line["content"]);
+                $line['content'] = rewrite_cached_urls($line['content']);
 
-					if ($line['note'])
-					    $line['note'] = Article::format_article_note($id, $line['note']);
-					else
-					    $line['note'] = "";
+                if ($line['note'])
+                    $line['note'] = Article::format_article_note($id, $line['note']);
+                else
+                    $line['note'] = "";
 
-					if (!get_pref("CDM_EXPANDED")) {
-						$line["cdm_excerpt"] = "<span class='collapse'>
-                            <i class='material-icons' onclick='return Article.cdmUnsetActive(event)'
-                                title=\"" . __("Collapse article") . "\">remove_circle</i></span>";
+                if (!get_pref("CDM_EXPANDED")) {
+                    $line["cdm_excerpt"] = "<span class='collapse'>
+                        <i class='material-icons' onclick='return Article.cdmUnsetActive(event)'
+                            title=\"" . __("Collapse article") . "\">remove_circle</i></span>";
 
-						if (get_pref('SHOW_CONTENT_PREVIEW')) {
-							$line["cdm_excerpt"] .= "<span class='excerpt'>" . $line["content_preview"] . "</span>";
-						}
-					}
+                    if (get_pref('SHOW_CONTENT_PREVIEW')) {
+                        $line["cdm_excerpt"] .= "<span class='excerpt'>" . $line["content_preview"] . "</span>";
+                    }
+                }
 
-					$line["enclosures"] = Article::format_article_enclosures($id, $line["always_display_enclosures"],
-						$line["content"], $line["hide_images"]);
+                $line["enclosures"] = Article::format_article_enclosures($id, $line["always_display_enclosures"],
+                    $line["content"], $line["hide_images"]);
 
-					if ($line["orig_feed_id"]) {
+                if ($line["orig_feed_id"]) {
 
-						$ofgh = $this->pdo->prepare("SELECT * FROM ttrss_archived_feeds
-                        WHERE id = ? AND owner_uid = ?");
-						$ofgh->execute([$line["orig_feed_id"], $_SESSION['uid']]);
+                    $ofgh = $this->pdo->prepare("SELECT * FROM ttrss_archived_feeds
+                    WHERE id = ? AND owner_uid = ?");
+                    $ofgh->execute([$line["orig_feed_id"], $_SESSION['uid']]);
 
-						if ($tmp_line = $ofgh->fetch()) {
-						    $line["orig_feed"] = [ $tmp_line["title"], $tmp_line["site_url"], $tmp_line["feed_url"] ];
-						}
-					}
+                    if ($tmp_line = $ofgh->fetch()) {
+                        $line["orig_feed"] = [ $tmp_line["title"], $tmp_line["site_url"], $tmp_line["feed_url"] ];
+                    }
                 }
 
-
+				$line["updated_long"] = make_local_datetime($line["updated"],true);
 				$line["updated"] = make_local_datetime($line["updated"], false, false, false, true);
+
+
 				$line['imported'] = T_sprintf("Imported at %s",
 					make_local_datetime($line["date_entered"], false));
 

+ 53 - 46
js/Article.js

@@ -137,58 +137,65 @@ define(["dojo/_base/declare"], function (declare) {
 			} catch (e) {
 			}
 		},
-		view: function (id, noexpand) {
-			this.setActive(id);
-
-			if (!noexpand) {
-				console.log("loading article", id);
+		formatComments: function(hl) {
+			let comments = "";
 
-				const cids = [];
-
-				/* only request uncached articles */
-
-				this.getRelativeIds(id).each((n) => {
-					if (!ArticleCache.get(n))
-						cids.push(n);
-				});
+			if (hl.comments) {
+				let comments_msg = __("comments");
 
-				const cached_article = ArticleCache.get(id);
-
-				if (cached_article) {
-					console.log('rendering cached', id);
-					this.render(cached_article);
-					return false;
+				if (hl.num_comments > 0) {
+					comments_msg = hl.num_comments + " " + ngettext("comment", "comments", hl.num_comments)
 				}
 
-				xhrPost("backend.php", {op: "article", method: "view", id: id, cids: cids.toString()}, (transport) => {
-					try {
-						const reply = App.handleRpcJson(transport);
-
-						if (reply) {
-
-							reply.each(function (article) {
-								if (Article.getActive() == article['id']) {
-									Article.render(article['content']);
-								}
-								ArticleCache.set(article['id'], article['content']);
-							});
-
-						} else {
-							console.error("Invalid object received: " + transport.responseText);
-
-							Article.render("<div class='whiteBox'>" +
-								__('Could not display article (invalid object received - see error console for details)') + "</div>");
-						}
-
-						//const unread_in_buffer = $$("#headlines-frame > div[id*=RROW][class*=Unread]").length;
-						//request_counters(unread_in_buffer == 0);
+				comments = `<a href="${hl.comments}">(${comments_msg})</a>`;
+			}
 
-						Notify.close();
+			return comments;
+		},
+		formatOriginallyFrom: function(hl) {
+			return hl.orig_feed ? `<span>
+					${__('Originally from:')} <a target="_blank" rel="noopener noreferrer" href="${hl.orig_feed[1]}">${hl.orig_feed[0]}</a>
+					</span>` : "";
+		},
+		view: function (id, noexpand) {
+			this.setActive(id);
 
-					} catch (e) {
-						App.Error.report(e);
-					}
-				})
+			if (!noexpand) {
+				const hl = Headlines.objectById(id);
+
+				if (hl) {
+
+					const comments = this.formatComments(hl);
+					const originally_from = this.formatOriginallyFrom(hl);
+
+					const article = `<div class="post post-${hl.id}">
+						<div class="header">
+							<div class="row">
+								<div class="title"><a target="_blank" rel="noopener noreferrer" title="${hl.title}" href="${hl.link}">${hl.title}</a></div>
+								<div class="date">${hl.updated_long}</div>
+							</div>
+							<div class="row">
+								<div class="buttons left">${hl.buttons_left}</div>
+								<div class="comments">${comments}</div>
+								<div class="author">${hl.author}</div>
+								<i class="material-icons">label_outline</i>
+								<span id="ATSTR-${hl.id}">${hl.tags_str}</span>
+								&nbsp;<a title="${__("Edit tags for this article")}" href="#" 
+									onclick="Article.editTags(${hl.id})">(+)</a>
+								<div class="buttons right">${hl.buttons}</div>
+							</div>
+						</div>
+						<div id="POSTNOTE-${hl.id}">${hl.note}</div>
+						<div class="content" lang="${hl.lang ? hl.lang : 'en'}">
+							${originally_from}
+							${hl.content}
+							${hl.enclosures}
+						</div>
+						</div>`;
+
+					Headlines.toggleUnread(id, 0);
+					this.render(article);
+				}
 			}
 
 			return false;

+ 0 - 29
js/ArticleCache.js

@@ -1,29 +0,0 @@
-'use strict'
-/* global __, ngettext */
-define(["dojo/_base/declare"], function (declare) {
-	ArticleCache = {
-		has_storage: 'sessionStorage' in window && window['sessionStorage'] !== null,
-		set: function (id, obj) {
-			if (this.has_storage)
-				try {
-					sessionStorage["article:" + id] = obj;
-				} catch (e) {
-					sessionStorage.clear();
-				}
-		},
-		get: function (id) {
-			if (this.has_storage)
-				return sessionStorage["article:" + id];
-		},
-		clear: function () {
-			if (this.has_storage)
-				sessionStorage.clear();
-		},
-		del: function (id) {
-			if (this.has_storage)
-				sessionStorage.removeItem("article:" + id);
-		},
-	}
-
-	return ArticleCache;
-});

+ 20 - 26
js/Headlines.js

@@ -4,7 +4,7 @@ define(["dojo/_base/declare"], function (declare) {
 	Headlines = {
 		vgroup_last_feed: undefined,
 		_headlines_scroll_timeout: 0,
-		loaded_article_ids: [],
+		headlines: [],
 		current_first_id: 0,
 		catchup_id_batch: [],
 		click: function (event, id, in_body) {
@@ -239,6 +239,9 @@ define(["dojo/_base/declare"], function (declare) {
 				}
 			}
 		},
+		objectById: function (id){
+			return this.headlines[id];
+		},
 		renderHeadline: function (headlines, hl) {
 			let row = null;
 
@@ -266,24 +269,11 @@ define(["dojo/_base/declare"], function (declare) {
 			if (App.isCombinedMode()) {
 				row_class += App.getInitParam("cdm_expanded") ? " expanded" : " expandable";
 
-				let originally_from = hl.orig_feed ? `<span>
-					${__('Originally from:')} <a target="_blank" rel="noopener noreferrer" href="${hl.orig_feed[1]}">${hl.orig_feed[0]}</a>
-					</span>` : "";
-
-				let comments = "";
-
-				if (hl.comments) {
-					let comments_msg = __("comments");
-
-					if (hl.num_comments > 0) {
-						comments_msg = hl.num_comments + " " + ngettext("comment", "comments", hl.num_comments)
-					}
-
-					comments = `<a href="${hl.comments}">(${comments_msg})</a>`;
-				}
+				const comments = Article.formatComments(hl);
+				const originally_from = Article.formatOriginallyFrom(hl);
 
 				row = `<div class="cdm ${row_class} ${hl.score_class}" id="RROW-${hl.id}" data-article-id="${hl.id}" data-orig-feed-id="${hl.feed_id}" 
-							data-content="${hl.content}" onmouseover="Article.mouseIn(${hl.id})" onmouseout="Article.mouseOut(${hl.id})">
+							data-content="${escapeHtml(hl.content)}" onmouseover="Article.mouseIn(${hl.id})" onmouseout="Article.mouseOut(${hl.id})">
 							
 							<div class="header">
 								<div class="left">
@@ -319,7 +309,9 @@ define(["dojo/_base/declare"], function (declare) {
 									
 							<div class="content" onclick="return Headlines.click(event, ${hl.id}, true);">
 								<div id="POSTNOTE-${hl.id}">${hl.note}</div>
-								<div class="content-inner" lang="${hl.lang ? hl.lang : 'en'}"></div>
+								<div class="content-inner" lang="${hl.lang ? hl.lang : 'en'}">
+									<img src="${App.getInitParam('icon_indicator_white')}">
+								</div>
 								<div class="intermediate">									
 									${hl.enclosures}
 								</div>
@@ -329,7 +321,7 @@ define(["dojo/_base/declare"], function (declare) {
 										${hl.buttons_left}
 										<i class="material-icons">label_outline</i>
 										<span id="ATSTR-${hl.id}">${hl.tags_str}</span>
-										<a title="Edit tags for this article" href="#" 
+										<a title="${__("Edit tags for this article")}" href="#" 
 											onclick="Article.editTags(${hl.id})">(+)</a>
 										${comments}
 									</div>
@@ -426,7 +418,7 @@ define(["dojo/_base/declare"], function (declare) {
 				this.current_first_id = reply['headlines']['first_id'];
 
 				if (offset == 0) {
-					this.loaded_article_ids = [];
+					//this.headlines = [];
 					this.vgroup_last_feed = undefined;
 
 					dojo.html.set($("toolbar-headlines"),
@@ -439,7 +431,10 @@ define(["dojo/_base/declare"], function (declare) {
 						$("headlines-frame").innerHTML = '';
 
 						for (let i = 0; i < reply['headlines']['content'].length; i++) {
-							this.renderHeadline(reply['headlines'], reply['headlines']['content'][i]);
+							const hl = reply['headlines']['content'][i];
+
+							this.renderHeadline(reply['headlines'], hl);
+							this.headlines[parseInt(hl.id)] = hl;
 						}
 					}
 
@@ -505,7 +500,10 @@ define(["dojo/_base/declare"], function (declare) {
 						$("headlines-frame").innerHTML = reply['headlines']['content'];
 					} else {
 						for (let i = 0; i < reply['headlines']['content'].length; i++) {
-							this.renderHeadline(reply['headlines'], reply['headlines']['content'][i]);
+							const hl = reply['headlines']['content'][i];
+
+							this.renderHeadline(reply['headlines'], hl);
+							this.headlines[parseInt(hl.id)] = hl;
 						}
 					}
 
@@ -1014,10 +1012,6 @@ define(["dojo/_base/declare"], function (declare) {
 				return;
 			}
 
-			for (let i = 0; i < rows.length; i++) {
-				ArticleCache.del(rows[i]);
-			}
-
 			const query = {op: "rpc", method: op, ids: rows.toString()};
 
 			xhrPost("backend.php", query, (transport) => {

+ 13 - 0
js/common.js

@@ -331,3 +331,16 @@ function popupOpenArticle(id) {
 		w.location = "backend.php?op=article&method=view&mode=raw&html=1&zoom=1&id=" + id + "&csrf_token=" + App.getInitParam("csrf_token");
 	}
 }
+
+// htmlspecialchars()-alike for headlines data-content attribute
+function escapeHtml(text) {
+	const map = {
+		'&': '&amp;',
+		'<': '&lt;',
+		'>': '&gt;',
+		'"': '&quot;',
+		"'": '&#039;'
+	};
+
+	return text.replace(/[&<>"']/g, function(m) { return map[m]; });
+}

+ 0 - 5
js/tt-rss.js

@@ -7,7 +7,6 @@ let Filters;
 let Feeds;
 let Headlines;
 let Article;
-let ArticleCache;
 let PluginHost;
 
 const Plugins = {};
@@ -54,7 +53,6 @@ require(["dojo/_base/kernel",
 	"fox/Feeds",
 	"fox/Headlines",
 	"fox/Article",
-	"fox/ArticleCache",
 	"fox/FeedStoreModel",
 	"fox/FeedTree"], function (dojo, declare, ready, parser, AppBase) {
 
@@ -138,8 +136,6 @@ require(["dojo/_base/kernel",
 
 					App.setLoadingProgress(50);
 
-					ArticleCache.clear();
-
 					this._widescreen_mode = App.getInitParam("widescreen");
 					this.switchPanelMode(this._widescreen_mode);
 
@@ -162,7 +158,6 @@ require(["dojo/_base/kernel",
 					document.title = tmp;
 				},
 				onViewModeChanged: function() {
-					ArticleCache.clear();
 					return Feeds.reloadCurrent('');
 				},
 				isCombinedMode: function() {

+ 0 - 2
plugins/note/note.js

@@ -18,8 +18,6 @@ Plugins.Note = {
 						dialog.hide();
 
 						if (reply) {
-							ArticleCache.del(id);
-
 							const elem = $("POSTNOTE-" + id);
 
 							if (elem) {