[Plugin] Sort depending on unread count

This plugin is mainly useful for subscriptions to forum topics, (Reddit) conversations, and feeds that often include follow-up articles.

Upon opening any feed it will make sure that if 1) your view mode is set to Adaptive and 2) there are unread articles, the sort order will be set to Oldest first. This way the conversation of the topic can be read top-top-bottom (or follow-up articles will be presented below previous ones). If (1) or (2) is not the case, it will switch back to the default sort order, so you can quickly find the articles that you have read last.

See unread_oldest_first in FeedMei/plugins.local at master · ltGuillaume/FeedMei · GitHub

A small issue remains with this plugin though: upon first load (or a browser refresh/F5), the unread count may read ‘0’ when checked by the plugin, even though later on the unread count turns out to be >0 when actually calculated. In this case, I can either wrongfully set the order to ‘Default’, or not do anything at all. So, as a workaround, I’ve specified that the plugin shouldn’t change the order upon first load when the unread count is 0.

What I would like is to be able to force a calculation somehow, wait for that and set the order accordingly.

counters are delayed on load, this is how how its supposed to work

you need to wait a bit for them to appear

Yes I know. Unfortunately there is no distinction between “counter is not ready yet” and “no unread messages”: in both instances, the returned value is ‘0’. Would be great if the returned value is ‘-1’ or null in case the calculation hasn’t been done yet, or if there’s a hook after calculation has been finalized.

yeah it’s a good idea, i’ll make a note to take a look at this tomorrow

https://git.tt-rss.org/fox/tt-rss/commit/4b74491b8b3353ae82f4dc1468e1c20546e5e03d

i haven’t found any other places where unread is assumed to be equal to zero, but it’s possible that this could introduce some minor issues. if something goes weird, report back.

e:

sure why not - https://git.tt-rss.org/fox/tt-rss/commit/6479c073244b4689c24ddd14febf3bfa7d0d6ed3

Thanks! I found that using Feeds.getUnread() somehow still returns 0 if the unread counts are not yet calculated, so I can’t really use that. I would rather make use of a -1 than of the HOOK_COUNTERS_PROCESSED, since - now it’s implemented - my tests indicate that it has a big delay: the feed is fully displayed first, then rendered again in the changed order.

However, I found that HOOK_FEED_SET_ACTIVE is triggered a second time upon load, and that the unread count is properly set that time. So I simply ignore the very first trigger, then start setting the order from the second trigger on.

So, what could be done is

  1. Implement that Feeds.getUnread() does return -1 when not calculated yet
  2. Or just revert the changes

e: when I select a non-virtual feed without unread articles, I see that HOOK_FEED_SET_ACTIVE is triggered 3 times (without my plugin intervening). The first 2 times the count is 0, then it’s -1. A bit odd.

that’s strange, initial value should be set as -1 as per getfeedtree. what kind of feed were you testing on?

e: some feeds (i.e. contents of Special category) may have their initial value set to 0 (or an actual unread amount) simply because counters are assigned on load already. counters are then loaded lazily for the rest of the tree.

maybe that’s because of lazy loading? i can change it so that hook is only invoked if feed actually changes although that would mean it is only being called when feed is requested asynchronously and no headlines are available (yet)

Here is some console output with comments:
Example of problem:
Fresh articles feed (18 unread articles):

HOOK_FEED_SET_ACTIVE | unread count = 0		// First time is ignored
HOOK_FEED_SET_ACTIVE | unread count = 0		// Second time should at least be -1, it's not
plugin: setOrder([-3, false]) | Unread = 0	// So I set the order according to a wrong unread count
HOOK_FEED_SET_ACTIVE | unread count = 0		// Count is still wrong
HOOK_FEED_SET_ACTIVE | unread count = 18	// There it is... But it's ignored by the plugin, because it already set the order according to the wrong unread count right before. I need to do that, because otherwise a manual change of order wouldn't be possible anymore (that also triggers HOOK_FEED_SET_ACTIVE).

Example of correct process:
Category ‘Uncategorized’ with 1 unread article:

HOOK_FEED_SET_ACTIVE | unread count = 0		// First time is ignored by plugin
HOOK_FEED_SET_ACTIVE | unread count = -1	// Now that it's -1, I register HOOK_COUNTERS_PROCESSED (sometimes the counter is already correct, so I'm not gonna register HOOK_COUNTERS_PROCESSED by default. Also, there's no way to UNREGISTER a hook.
HOOK_COUNTERS_PROCESSED | unread count = 1	// There we go, it's 1
plugin: setOrder([0, true]) | Unread = 1	// Order is set accordingly
HOOK_FEED_SET_ACTIVE | unread count = 1		// Triggered by the reorder, ignored by the plugin

The full (JavaScript part of the) plugin right now:

require(['dojo/_base/kernel', 'dojo/ready'], function (dojo, ready) {
	ready(function () {
		this.prev_feed = null;
		
		function setOrder(feed) {
			console.log('plugin:setOrder(['+ feed[0] +', '+ feed[1] +']) | Unread = '+ Feeds.getUnread(feed[0], feed[1]));
			if (document.forms["toolbar-main"].view_mode.value == 'adaptive') {
				let order = Feeds.getUnread(feed[0], feed[1]) > 0 ? 'date_reverse' : 'default';
				if (order != document.forms["toolbar-main"].order_by.value)
					dijit.getEnclosingWidget(document.forms["toolbar-main"].order_by).attr('value', order);
			}
			this.prev_feed = feed[0];
		}

		PluginHost.register(PluginHost.HOOK_FEED_SET_ACTIVE, (feed) => {
			console.log('HOOK_FEED_SET_ACTIVE | unread count = '+ Feeds.getUnread(feed[0], feed[1]));
			if (!this.prev_feed)
				return this.prev_feed = -99;

			if (Feeds.getUnread(feed[0], feed[1]) == -1)
				return PluginHost.register(PluginHost.HOOK_COUNTERS_PROCESSED, () => {
					console.log('HOOK_COUNTERS_PROCESSED | unread count = '+ Feeds.getUnread(feed[0], feed[1]));
					if (this.prev_feed == -99)
						setOrder([Feeds.getActive(), Feeds.activeIsCat()]);
				});

			if (feed[0] != this.prev_feed)
				setOrder(feed);
		});
	});
});

if there’s hash parameters in the URL setActive() is called before tree has loaded, i suppose, that’s why there’s no data (yet). maybe this should happen later when loading.

looks like in this situation (tree and model is available but no data has been received) FeedStoreModel.getFeedUnread() falls back to 0, it should return -1.

you can try changing FeedStoreModel.js:34 to return -1 instead of 0 and see if that fixes things.

i made a small change which should stop Feeds.setActive() to be called multiple times by moving hash handling later during startup (to feedlist init) which simplifies things a little bit.

https://git.tt-rss.org/fox/tt-rss/commit/9368f1a07f51b58afe7c4665164b20128d22c7bb

i’m not sure if FeedStoreModel change is still needed although it probably should be changed for consistency.

e: i did some quick testing and it seems that unread counter is available for feed -3 when HOOK_FEED_SET_ACTIVE is called.

I found that -3 is often availabe quickly (so the new counts hook wouldn’t be used by the plugin then), while feed counts lower than -3 aren’t available for a while. But I’ll check if your changes in a moment. In the meantime I added the following to my test instance, simplifies things afterwards:

Added to Pluginhost.js:

	unregister: function (name, callback) {
		for (var i = 0; i < this.hooks[name].length; i++)
			if (this.hooks[name][i] == callback)
				this.hooks[name].splice(i, 1);
	}

This way, the plugin hook that only has to be run once can unregister itself with PluginHost.unregister(PluginHost.HOOK_COUNTERS_PROCESSED, functionVar);

Which makes the plugin like this:

require(['dojo/_base/kernel', 'dojo/ready'], function (dojo, ready) {
	ready(function () {
		this.prev_feed = null;
		
		function setOrder(feed) {
			console.log('unread_oldest_first: setOrder(['+ feed[0] +', '+ feed[1] +']) | Unread = '+ Feeds.getUnread(feed[0], feed[1]));
			if (document.forms["toolbar-main"].view_mode.value == 'adaptive') {
				let order = Feeds.getUnread(feed[0], feed[1]) > 0 ? 'date_reverse' : 'default';
				if (order != document.forms["toolbar-main"].order_by.value)
					dijit.getEnclosingWidget(document.forms["toolbar-main"].order_by).attr('value', order);
			}
			this.prev_feed = feed[0];
		}

		PluginHost.register(PluginHost.HOOK_FEED_SET_ACTIVE, (feed) => {
			console.log('unread_oldest_first: HOOK_FEED_SET_ACTIVE | unread count = '+ Feeds.getUnread(feed[0], feed[1]));
			if (!this.prev_feed)
				return this.prev_feed = 'first';	// Ignore first unread count

			if (this.prev_feed == 'first' && Feeds.getUnread(feed[0], feed[1]) == -1) {
				var countersHook = function() {
					console.log('unread_oldest_first: HOOK_COUNTERS_PROCESSED | unread count = '+ Feeds.getUnread(feed[0], feed[1]));
					if (this.prev_feed == 'hooked')
						setOrder([Feeds.getActive(), Feeds.activeIsCat()]);
					PluginHost.unregister(PluginHost.HOOK_COUNTERS_PROCESSED, countersHook);
				}
				PluginHost.register(PluginHost.HOOK_COUNTERS_PROCESSED, countersHook);
				return this.prev_feed = 'hooked';
			}

			if (feed[0] != this.prev_feed)
				setOrder(feed);
		});
	});
});

The code above only works if the change in FeedStoreModel.js you proposed (0 to -1) is done.

After updating to https://git.tt-rss.org/fox/tt-rss/commit/9368f1a07f51b58afe7c4665164b20128d22c7bb, when I refresh the browser, it does indeed reduce the amount of times setActive is called, so I could remove

if (!this.prev_feed)
	return this.prev_feed = 'first';	// Ignore first unread count

from the plugin.

There’s an issue with that latest commit: it tends to default to feed 0 (archived articles), even though another feed was selected / in the URL. Just switch feeds a couple of times and refresh in between, it’ll start jumping to archived articles after a while.

i wasn’t able to replicate this (using chrome here).

I’m using Waterfox (i.e. Firefox v56) as daily driver and disabled this plugin (and later on all plugins) to be sure it wasn’t an interaction between the plugin and the commit.

I’ve also tried in Chromium v74 and Firefox 69, and in all cases https://rss.mydomain.x/#f=-3&c=0 turned to https://rss.mydomain.x/#f=0&c=0 on a refresh.

Perhaps it’s a race condition issue that happens to show on my specific system.

e: No, the same happens on two other systems. The installation is as vanilla as it gets, no plugins, and it exhibits this behavior ONLY in the latest commit.

My bet is it’s best to just revert https://git.tt-rss.org/fox/tt-rss/commit/9368f1a07f51b58afe7c4665164b20128d22c7bb

no, we’re not doing that. the way it is now is cleaner and easier to understand, if there’s a race condition it’s better to figure out why it happens. in any case it’s not a high priority issue, restoring active feed on ctrl-r is mostly useful for debugging, it’s not like you need to reload tt-rss all the time.

It’s not just when Ctrl+R’ing, it also happens when I explicitly enter https://rss.mydomain.x/#f=-3&c=0 in a new tab.

e: Waterfox console after typing in the url https://rss.mydomain.x/#f=-3&c=0

xhrPost: Object { op: "rpc", method: "sanityCheck", hasAudio: true, hasMp3: true, clientTzOffset: -7200, hasSandbox: true }  common.js:20:2
sanity check ok  AppBase.js:314:4
reading init-params...  AppBase.js:319:5
IP: on_catchup_show_next_feed => 0  AppBase.js:349:7
IP: hide_read_feeds => 0  AppBase.js:349:7
IP: enable_feed_cats => 1  AppBase.js:349:7
IP: feeds_sort_by_unread => 0  AppBase.js:349:7
IP: confirm_feed_catchup => 1  AppBase.js:349:7
IP: cdm_auto_catchup => 0  AppBase.js:349:7
IP: fresh_article_max_age => 720  AppBase.js:349:7
IP: hide_read_shows_special => 1  AppBase.js:349:7
IP: combined_display_mode => 0  AppBase.js:349:7
IP: check_for_updates => true  AppBase.js:349:7
IP: icons_url => cache/feed-icons  AppBase.js:349:7
IP: cookie_lifetime => 31536000  AppBase.js:349:7
IP: default_view_mode => adaptive  AppBase.js:349:7
IP: default_view_limit => 30  AppBase.js:349:7
IP: default_view_order_by => date_reverse  AppBase.js:349:7
IP: bw_limit => 0  AppBase.js:349:7
IP: is_default_pw => false  AppBase.js:349:7
IP: label_base_index => -1024  AppBase.js:349:7
IP: theme => feedmei+.css  AppBase.js:349:7
IP: plugins => Auth_Internal  AppBase.js:349:7
IP: php_platform => Linux  AppBase.js:349:7
IP: php_version => 7.2.13  AppBase.js:349:7
IP: sanity_checksum => f779d1b4bc060e5a687515d2aee604915ee115d8  AppBase.js:349:7
IP: max_feed_id => 899  AppBase.js:349:7
IP: num_feeds => 88  AppBase.js:349:7
IP: hotkeys => Array [ Array[4], Array[0] ]  AppBase.js:349:7
IP: csrf_token => bfxi4j5cb24f6be99e5  AppBase.js:349:7
IP: widescreen => 0  AppBase.js:349:7
IP: simple_update => false  AppBase.js:349:7
IP: icon_indicator_white => "data:image/gif;base64,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"  AppBase.js:349:7
IP: labels => Array [  ]  AppBase.js:349:7
setActive 0 Article.js:292:4
xhrPost: Object { op: "rpc", method: "setpanelmode", wide: 0 }  common.js:20:2
second stage ok  tt-rss.js:169:6
reloadCurrent:   Feeds.js:102:4
notify Loading, please wait... 3  common.js:189:3
setActive 0 false  Feeds.js:256:4
xhrPost: Object { op: "feeds", method: "view", feed: 0, view_mode: "adaptive", order_by: "date_reverse", m: "ForceUpdate", cat: false }  common.js:20:2
RI: max_feed_id => 899  AppBase.js:262:6
RI: num_feeds => 88  AppBase.js:262:6
RI: cdm_expanded => false  AppBase.js:262:6
RI: labels => Array [  ]  AppBase.js:262:6
RI: recent_log_events => 0  AppBase.js:262:6
Headlines.onLoaded: offset= 0 append= false  Headlines.js:545:4
received 1 headlines, infscroll disabled= true  Headlines.js:562:5
setActive 0  Article.js:292:4
in feedlist init  Feeds.js:198:4
setActive 0 false  Feeds.js:256:4
xhrPost: Object { op: "feeds", method: "view", feed: "0", view_mode: "adaptive", order_by: "date_reverse", m: "ForceUpdate", cat: false }  common.js:20:2
RI: max_feed_id => 899  AppBase.js:262:6
RI: num_feeds => 88  AppBase.js:262:6
RI: cdm_expanded => false  AppBase.js:262:6
RI: labels => Array [  ]  AppBase.js:262:6
RI: recent_log_events => 0  AppBase.js:262:6
Headlines.onLoaded: offset= 0 append= false  Headlines.js:545:4
received 1 headlines, infscroll disabled= true  Headlines.js:562:5
setActive 0  Article.js:292:4
xhrPost: Object { op: "rpc", method: "getAllCounters", seq: 1 }
notify Loading, please wait... 3  common.js:189:3
setActive 0 false  Feeds.js:256:1234: 
...
in feedlist init  Feeds.js:198:4

it seems that something is calling Feeds.open() before feedlist init, so hash parameters get reset. not sure what it could be tbh, a trace would probably help.

that’s also interesting, it might be caused by view mode toolbar being changed on load. a most likely cause.

e: yeah, that’s why it happens. i’ll take a look on how to best fix this either later today or tomorrow.