(Download KeepUnread plugin for Tiny Tiny RSS)

It's rare I sign up to forums, but every now and again I find a reason to do so.  I've been using tt-rss ever since Google announced the closure of Google Reader many months ago.  It's a great piece of software that met nearly all my requirements for an RSS reader.  However there was one ommision that broke my usual workflow.  

When I'm reading through articles occasionally I'll come across one that I don't want to read at that point in time.  Maybe it was too long, maybe it was about a subject that I really didn't want to get into at that time – the point is that in Google Reader I could simply skip over to the next article and click "Keep Unread" to prevent Google Reader automatically marking it as read as I scrolled past.  The next time I looked at that feed the article would still be there, waiting for me to read it.

As far as I could tell, tt-rss didn't have a comparable feature, so I signed up to the forums to ask about this feature and was met with a tirade of sarcasm, abuse and rudeness before my thread was locked and dumped into "Comedy Bistro".  It has somewhat tainted my opinion of tt-rss to be honest.  There seems to be a lot of snobbery regarding feature requests from ex (or soon to be ex) Google Reader users.

Well if you want to keep stuff unread you gonna have to go ahead and not read it, eh

-blainemono

Oh the irony, if only they'd actually read what I had posted.  Which was that I wanted a way to prevent the software assuming I had read something that I had not:)

Just so you know, as far as I can tell, your desire to not have things marked-as-read when you click them makes you an insufferable buffoon who wouldn't know good UX if it licked you in the pucker. Please, don't shoot the messenger (me).

– koalabear7410

This one above wasn't aimed at me, but was in a similar thread.  Don't you just love know-it-all's.  If their position wasn't so preposterous ("Well you clicked on the screen, that's the same thing as reading 1000+ words") then the attitude might be excusable.  No hang on a minute, what am I thinking?  It'd still be inexcusable.

Greater Internet Fuckwad Theory

I just spent, like, 10 minutes trying to decide what I should reply with:
 

  • A helpful explanation that fox has never implemented a feature "because Google Reader"
  • A sarcastic explanation that fox has never implemented a feature "because Google Reader"
  • A paragraph lamenting that people no longer "lurk before posting"
  • A picture of a bear

-craywolf

So, because I'm asking for a feature from Google Reader I am asking for it because Google Reader has it?  That's one hell of an assumption.  Again, completely unnecessary, and unprovoked.  As for the snide "people no longer lurk before posting" comment, I had been lurking for months.  It doesn't take a rocket scientist to consider that perhaps I only registered when I had something to post when the forums do not require a sign-in unless you wish to post.  This is even more rediculous when you consider that by default tt-rss will add a feed for new posts on tt-rss forums (which had been my perferred method of lurking up 'til now).

But my favourite reply was this one:

until then, try starring things you want to read later.

also, 'keep unread' on google reader wasn't as good as you seem to think, because the next time you saw that article, you'd have to mark 'keep unread' again, or it'd disappear. stars in ttrss work better.

– sleeper_service

Yes, that's right.  I was wrong, this feature I have been using for as long as I can even remember, wasn't as good as I thought it was..  As it happens I use stars to mark articles that I think are really good, which makes it easier to find them again in the future.  I use "Keep Unread" for articles that I have not yet read so that they appear in my reading list the next time I view that category.  The fact that this person proceeds to explain how this feature of Google Reader served exactly the purpose I used it for, and was enquiring about the availability of within tt-rss, is worth bonus obnoxious troll points surely:)

Now, on to the good stuff:

So yeah, despite the thoroughly unwelcoming welcome I received to the community I decided I'd still put something back into it.  It took around an hour and a half this morning, but I now have "Keep Unread" implemented exactly how I want it in my installation of tt-rss.  This means that once checked, Keep Unread will prevent that article being automatically marked as read:

  • When you click on the article contents
  • When you scroll past the article
  • When you click on a later article in the feed
  • When you use keyboard shortcut "j" to skip to next article

You will also be unable to toggle the read status from unread to read while this checkbox is enabled.

I have only verified the use cases that I'm interested in (above), if you try this plugin and run into problems in certain scenarios drop me an e-mail and I'll be happy to look into it/add support for any other use cases where "Keep Unread" needs to over-ride the default behaviour.

If you wish to try it out for yourself you will need to install & enable my KeepUnread plugin for Tiny Tiny RSS and modify the following functions in feedlist.js as below.  For what it's worth I am using version 1.7.8, so you may need to tweak this slightly if you are using an earlier/later version.  

The plugin archive contains the modified version of feedlist.js  If you prefer, you can use this to over-write the original (assuming you haven't made any other modifications to the file that you wish to retain).

function showArticleInHeadlines(id) {
    try {
        selectArticles("none");

        var crow = $("RROW-" + id);

        if (!crow) return;

        var article_is_unread = crow.hasClassName("Unread");
        var keep_unread = crow.hasClassName("KeepUnread");
        
        if (!keep_unread) {
            crow.removeClassName("Unread");
        }
        crow.addClassName("active");

        selectArticles('none');

        var view_mode = false;

        try {
            view_mode = document.forms['main_toolbar_form'].view_mode;
            view_mode = view_mode[view_mode.selectedIndex].value;
        } catch (e) {
            //
        }

        markHeadline(id);

        if (article_is_unread && !keep_unread)
            _force_scheduled_update = true;

    } catch (e) {
        exception_error("showArticleInHeadlines", e);
    }
}
function toggleUnread(id, cmode, effect) {
    try {
        var row = $("RROW-" + id);
        if (row) {
            var keep_unread = row.hasClassName("KeepUnread");
            if (cmode == undefined || cmode == 2) {
                if (row.hasClassName("Unread")) {
                    if (!keep_unread) {
                        row.removeClassName("Unread");
                    }
                } else {
                    row.addClassName("Unread");
                }
            } else if (cmode == 0) {
                if (!keep_unread) {
                    row.removeClassName("Unread");
                }
            } else if (cmode == 1) {
                row.addClassName("Unread");
            }

            if (cmode == undefined) cmode = 2;

            if (!keep_unread) {
                var query = "?op=rpc&method=catchupSelected&cmode=" + param_escape(cmode) + "&ids=" + param_escape(id);
                new Ajax.Request("backend.php", {
                    parameters: query,
                    onComplete: function(transport) {
                        handle_rpc_json(transport);
                    } });
            }
        }

    } catch (e) {
        exception_error("toggleUnread", e);
    }
}
function selectionToggleUnread(set_state, callback, no_error, ids) {
    try {
        var rows = ids ? ids : getSelectedArticleIds2();

        if (rows.length == 0 && !no_error) {
            alert(__("No articles are selected."));
            return;
        }

        for (var i = 0; i < rows.length; i++) {
            var row = $("RROW-" + rows[i]);
            var keep_unread = row.hasClassName("KeepUnread");
            if (row) {
                if (set_state == undefined) {
                    if (row.hasClassName("Unread")) {
                        if (!keep_unread){
                            row.removeClassName("Unread");
                        }
                    } else {
                        row.addClassName("Unread");
                    }
                }

                if (set_state == false && !keep_unread) {
                    row.removeClassName("Unread");
                }

                if (set_state == true) {
                    row.addClassName("Unread");
                }
            }
        }

        if (rows.length > 0) {

            var cmode = "";

            if (set_state == undefined) {
                cmode = "2";
            } else if (set_state == true) {
                cmode = "1";
            } else if (set_state == false) {
                cmode = "0";
            }

            var query = "?op=rpc&method=catchupSelected" +
                "&cmode=" + cmode + "&ids=" + param_escape(rows.toString());

            notify_progress("Loading, please wait...");

            new Ajax.Request("backend.php", {
                parameters: query,
                onComplete: function(transport) {
                    handle_rpc_json(transport);
                    if (callback) callback(transport);
                } });

        }

    } catch (e) {
        exception_error("selectionToggleUnread", e);
    }
}
function catchupBatchedArticles() {
    try {
        if (catchup_id_batch.length > 0 && !_infscroll_request_sent) {
            // make a copy of the array
            var tmp_batch = catchup_id_batch.slice();
            console.log("Removing 'keep unread' articles from batch ...");
            console.log("Batch before = " + tmp_batch + " (" +tmp_batch.length + ")");
            var batch = new Array();
            for (var i = 0; i < tmp_batch.length; i++) {
                id = tmp_batch[i];
                console.log("Processing [" + i + "] - id:" + id + "....");
                var elem = $("RROW-" + id);
                if (elem && !elem.hasClassName("KeepUnread")) {
                    batch.push(id);
                }
            }
            console.log("Batch after = " + batch + " (" +batch.length + ")")
            if (batch.length == 0) {
                return;
            }
            
            var query = "?op=rpc&method=catchupSelected" +
                "&cmode=0&ids=" + param_escape(batch.toString());
            console.log(query);

            new Ajax.Request("backend.php", {
                parameters: query,
                onComplete: function(transport) {
                    handle_rpc_json(transport);

                    reply = JSON.parse(transport.responseText);
                    var batch = reply.ids;

                    batch.each(function(id) {
                        console.log(id);
                        var elem = $("RROW-" + id);
                        if (elem) elem.removeClassName("Unread");
                        catchup_id_batch.remove(id);
                    });

                } });
        }

    } catch (e) {
        exception_error("catchupBatchedArticles", e);
    }
}
function catchupRelativeToArticle(below, id) {
    try {
        if (!id) id = getActiveArticleId();
        if (!id) {
            alert(__("No article is selected."));
            return;
        }
        var visible_ids = getVisibleArticleIds();
        var ids_to_mark = new Array();

        if (!below) {
            for (var i = 0; i < visible_ids.length; i++) {
                if (visible_ids[i] != id) {
                    var e = $("RROW-" + visible_ids[i]);
                    if (e && e.hasClassName("Unread")) {
                        ids_to_mark.push(visible_ids[i]);
                    }
                } else {
                    break;
                }
            }
        } else {
            for (var i = visible_ids.length-1; i >= 0; i--) {
                if (visible_ids[i] != id) {
                    var e = $("RROW-" + visible_ids[i]);

                    if (e && e.hasClassName("Unread")) {
                        ids_to_mark.push(visible_ids[i]);
                    }
                } else {
                    break;
                }
            }
        }

        if (ids_to_mark.length == 0) {
            alert(__("No articles found to mark"));
        } else {
            var msg = ngettext("Mark %d article as read?", "Mark %d articles as read?", ids_to_mark.length).replace("%d", ids_to_mark.length);

            if (getInitParam("confirm_feed_catchup") != 1 || confirm(msg)) {
                for (var i = 0; i < ids_to_mark.length; i++) {
                    var e = $("RROW-" + ids_to_mark[i]);
                    if (e.hasClassName("KeepUnread")) {
                        e.removeClassName("Unread");
                    }
                }

                var query = "?op=rpc&method=catchupSelected" +
                    "&cmode=0" + "&ids=" + param_escape(ids_to_mark.toString());
                new Ajax.Request("backend.php", {
                    parameters: query,
                    onComplete: function(transport) {
                        handle_rpc_json(transport);
                    } });

            }
        }

    } catch (e) {
        exception_error("catchupRelativeToArticle", e);
    }
}
function cdmClicked(event, id) {
    try {
        //var shift_key = event.shiftKey;

        if (!event.ctrlKey) {

            if (!getInitParam("cdm_expanded")) {
                return cdmExpandArticle(id);
            } else {

                var elem = $("RROW-" + getActiveArticleId());

                if (elem) elem.removeClassName("active");

                selectArticles("none");
                toggleSelected(id);

                var elem = $("RROW-" + id);
                var article_is_unread = elem.hasClassName("Unread");
                var keep_unread = elem.hasClassName("KeepUnread");

                if (!keep_unread) {
                    elem.removeClassName("Unread");
                }
                elem.addClassName("active");

                setActiveArticleId(id);

                if (article_is_unread && !keep_unread) {
                    decrementFeedCounter(getActiveFeedId(), activeFeedIsCat());
                }
                
                if (!keep_unread) {
                    // Don't update this when user clicks on it if keep unread is enabled
                    var query = "?op=rpc&method=catchupSelected&cmode=0&ids=" + param_escape(id);

                    new Ajax.Request("backend.php", {
                        parameters: query,
                        onComplete: function(transport) {
                            handle_rpc_json(transport);
                        } });
                }
                return !event.shiftKey;
            }

        } else {
            toggleSelected(id, true);

            var elem = $("RROW-" + id);
            var article_is_unread = elem.hasClassName("Unread");

            if (article_is_unread) {
                decrementFeedCounter(getActiveFeedId(), activeFeedIsCat());
            }

            toggleUnread(id, 0, false);

            openArticleInNewWindow(id);
        }

        var unread_in_buffer = $$("#headlines-frame > div[id*=RROW][class*=Unread]").length
        request_counters(unread_in_buffer == 0);

    } catch (e) {
        exception_error("cdmClicked");
    }

    return false;
}