Skip to content

Comments improvements - #2476

Merged
tf merged 29 commits into
codevise:masterfrom
tf:comments-feed
Aug 27, 2026
Merged

Comments improvements#2476
tf merged 29 commits into
codevise:masterfrom
tf:comments-feed

Conversation

@tf

@tf tf commented Aug 27, 2026

Copy link
Copy Markdown
Member

REDMINE-21261

Slate splits a decorated range wherever another decoration overlaps it,
and every piece carries the range's key - so a range that another thread
overlaps registers several anchor elements under one key. The registry
held one element per key and deleted the key outright when an element
went away, which let a piece unmounting take down the anchor its
siblings still provided. The badge then had no reference to position
against and rendered nothing.

Revealing a resolved thread and hiding it again is enough to trigger it:
the highlight comes and goes, the leaves around it are split and merged,
and one of the overlapped threads loses its badge until something else
makes its leaf re-register.

The key now holds every element registered under it, and the badge
anchors to whichever comes first in the document - where the range
starts, rather than wherever the last piece to mount happened to be.
@tf tf added this to the v17.2 milestone Aug 27, 2026
tf added 26 commits August 27, 2026 15:26
Two words for the same thing had grown up side by side: the badges and
the entry list's dot speak of unread comments, the markers inside a
thread of new ones. The activity feed puts them in one view, where
"new" reads as recently written - which is what the feed's rows are
about, while the markers are about what the reviewer has read. The two
counts part ways as soon as a thread was read halfway, so the words
have to as well.

Everything that means "not read yet" is unread now, down to the summary
behind the entry list's dot: #new? becomes #unread?, and its counts
follow.
The chevron sat in the thread's corner, far from the replies it hid and
in the way of everything else that wants that corner. The count of the
replies now carries it: clicking the count no longer makes it disappear
into what it opened, it turns the chevron around instead, so the same
control closes the thread again. The faces of who replied still go once
the replies they stand in for are on screen.

A thread alone in its list used to be exempt from collapsing, which
left its count line as a control that did nothing. It is picked as the
initially expanded thread instead, so the same line closes it again.
Lists with more than one thread still open with all of them summarized,
and the entry-wide list keeps every thread that way: it renders a list
per subject, where one expanded thread each adds up.

Freeing the corner lets the comments' menus have it, lined up in a
column at the thread's right edge.
Clients read position as meaning: the first comment of a thread is the
topic and the rest are replies. Nothing guaranteed that order so far -
it held because InnoDB returns rows of a secondary index in primary key
order, not because the query asked for it.

No behavior change on the database Pageflow runs on, so the spec is a
regression guard rather than a failing test made to pass.
The thread JSON starts carrying resolvedById and resolverName, so that
clients can say who resolved a thread rather than only when it happened.

Both attributes already existed on the model; only the JSON left them
out.
Moves formatDate and formatDateTime out of Comment.js so that other
review components can render timestamps the same way.
The resolve button says a thread is resolved, but not by whom. Threads
carry the resolver since the review index started naming them, so every
list that shows a thread can now close it off with a line of its own -
the popover and the editor's lists included, which had no way of telling
who ended a discussion.
Answers what happened since the reviewer last looked, which neither the
structural list nor the toolbar arrows can: both are ordered by document
position, not by time.

One entry per thread, ordered by the thread's most recent event. A thread
rather than an event is the unit because both kinds of context a reviewer
needs - the content a comment is about and the discussion around it -
belong to the thread; per event, a thread that gained four replies would
repeat itself four times over. Where a thread sits in the entry is left to
the thread as well, so the entry carries it rather than a description of
it.

Lifts the unread predicate out of unreadComments so that the feed marks
entries by the same rule - baseline included - rather than restating it,
and so that the server side summary has a named counterpart to mirror.
Resolutions have no read mark of their own and go by their thread's.
Commenting on a thread or resolving one makes it the most recent again,
which pushed it to the top of the feed while the reviewer was working in
it - reordering the list under their own hands, for something they had
just done themselves. Threads now keep the place they had when the list
appeared, and coming back to the feed is what reflects the new order.

Ordering by the latest activity of others instead would have made the
rule depend on whether anyone else had touched a thread: a reply to your
own thread would still reorder it, a reply to someone else's would not.
Lets a caller show the opening comment and the tail of a discussion with
the replies in between folded into a marker, so a thread can be embedded
where there is no room for all of it.

A folded thread is a partial view, which is why it neither marks itself
read nor offers the reply form. Read state is one timestamp per thread,
so marking it would cover comments that were never shown; and replying
to a discussion seen only in part invites talking past it. Expanding
through the marker settles both.
Each row says what happened and embeds the thread it happened in, with
the earlier replies folded away. The latest reply stays visible: the
headline announces it, so the row has to show it.

Embedding the thread rather than a copy of its text puts both kinds of
context a reviewer needs in one place: the row reveals the content the
comments are about, and the fold marker reads the discussion without
leaving the list. Where the thread sits and what it quotes are left out
entirely - both are a click away, and saying them here only crowded the
row.

The thread carries the click and the highlight, as it does in the
structural list, so a row needs no frame of its own. Only the actor's
name is emphasized, the way a comment header does it.

Shows 30 rows and extends on demand rather than capping the list
silently.
Heads the rows of each day, with today and yesterday named rather than
dated. Structure the list was missing: rows carried a timestamp each,
which said less than a heading does and repeated what the discussion in
every thread already dates.

The heading also holds the machine readable date the rows gave up, and
formats from a timestamp within the day rather than from the day itself -
a bare date parses as UTC midnight, which reads as the day before west
of it.

Groups the shown slice rather than every entry, so a day spanning the
show more boundary is not headed twice.
Each row carries the dot that badges and threads already use one level
up, and unfolds its thread from the first unread reply rather than showing
only the latest one, so the unread part reads in the context of the
comment it answers.

No separator between seen and unseen rows: read marks are per thread, so
a line drawn through a global time order lands somewhere arbitrary as soon
as part of what is above it has been read. The date headings give the list
its structure and the per row dots carry the exact signal.
Puts the feed on scrolled/comments/activity with a back link, rather than
into a third tab of the comments view: the feed is not a scope of the
comments list but a different way of looking at all of them.

Clicking a row selects its thread the same way the structural list does,
so the preview scrolls to it and opens its popover. Since the preview
reports selected section threads without a highlighted id, the view keeps
the id of the clicked row to survive the round trip.
Adds a control strip above the tabs holding a link to the activity route,
carrying the dot the comments main menu item already uses so that unseen
comments are visible from within the comments view too.

The strip is laid out with room on the left: the resolution filter the
preview's floating toolbar offers is the intended next occupant. It stays
outside the sticky tab bar until it actually holds filters.
Selecting a subject in the preview keeps the sidebar where it is once it
is already showing comments, so that stepping through them does not tear
the reviewer off the tab they chose. The activity feed sits under the
same route but lists every subject at once, so a subject picked there had
nowhere to appear, and the selection went unanswered.

Threads clicked in the sidebar are the exception: they travel to the
preview as a message and come back as a selection, which now says so, so
that the view that asked is left alone.
A collapsed thread showed nothing of its resolution: the row carrying it
was hidden along with the replies, so a list of resolved threads read as
a list of ordinary ones. The state now stays whatever the thread shows of
itself, undoing it included. Only the offer to resolve keeps waiting for
the thread to be expanded.
Stepping to the next comment and jumping to a particular one differ only
in how they pick the target, not in what selecting it involves.
The feed the editor sidebar shows on its own route was out of reach while
reviewing in the preview. A toolbar button now opens it as a panel above
the toolbar, and clicking an entry reveals the thread in place - jumping
to it the way the arrows do, excursions and all, and switching the filter
where a resolved thread would otherwise stay behind the toolbar's own.

The panel goes into the portal layer that sits above the navigation
widgets, since it grows tall enough to reach them. That also keeps it out
of the toolbar, whose collapse would otherwise snapshot it, and clear of
the floating ui layer the popovers use, which is below the toolbar and
would swallow it.
The puck counted unread comments while the activity button beside it
counts topics carrying something new, so the same toolbar could show two
markers that disagree - a thread someone resolved without commenting lit
one and not the other.
A row in the feed shows its thread already, so opening the popover on top
of it said everything twice and covered the content the click had just
revealed. The selection now carries whether it only reveals: the badge
goes active and the subject scrolls into view, while the thread list
stays where the reviewer was reading it.

Clicking the badge or the highlight of a revealed subject opens the
popover after all, on the thread it was revealed for, rather than
clearing the selection or doing nothing.
A thread with no replies yet shows its reply form even where the list
keeps every thread summarized - there is nothing folded away to hide it.
Sending the reply gave the thread its first one, and with it the fold
that swallowed what had just been written; the draft held the thread
open only until the session dropped it.

The list now takes the reply as the reviewer picking that thread, the
same way clicking its count does.
Being on screen used to be enough, and anything the thread kept out of
sight - collapsed or folded replies - switched it off. Both halves were
wrong at the ends of this stack. The feed folds away exactly the replies
the reviewer has already seen, so a row it could never clear was hiding
nothing; and a whole entry list scrolled past clears markers the reviewer
navigates by, spending them on threads nobody looked at.

A thread is read once everything unread in it has been on screen, and,
in the lists that survey many threads at once, once the reviewer has
picked it out. That the entry list wants its threads expanded before
they count, and the feed does not, falls out of the first half: the
fold hides nothing unread, a collapsed thread hides its replies.
The dot in the corner locates one unread thread among several, which is
what it was for, but it takes finding. A thread whose topic has not been
read is new all through, so the whole card can say so: its border takes
a wash of the dot's colour, and the folded seam with it. A thread that
only gained replies keeps its border - what is new there is the count
and the divider, not the conversation.

The warning colour gains the lighter and lightest variants every other
colour in the palette already has.

The outline outranks the hover and highlight borders. What is left to
read is worth more than where the reviewer is, which the highlight halo
goes on showing either way.
The badges in the text column build their own, since they resolve a
thread from a Slate range rather than from a subject, and so they never
passed the badge an unread count. The dot stayed off, and worse, the
escape that keeps a badge with unseen comments from shrinking to a dot
never fired: unread text comments were the least visible ones in the
editor.

Badge takes the count now instead of a flag and the sentence naming it,
which both callers were otherwise left to phrase alike.
Read state froze for as long as a list was on screen, which kept a
thread marked new long after it had been read: the reviewer worked
through a list and nothing came off it until the list went away. The
freeze is there so markers do not vanish mid-read, and that only asks
for them to hold still while a thread is being read - not for the whole
visit. The list now freezes anew whenever the reviewer expands another
thread or collapses the one they were reading, so what they just read
loses its marker as they move on.

The popover's snapshot goes with it. It wrapped the badge and the list
together, and since a nested snapshot defers to the one outside it, the
list could never refresh. The badge has no call to hold read state of
its own either: it is the one thing in there the reviewer is not
reading, and it says whether a subject still has something left - which
is worth more the moment it stops saying so. The badges in the editor
have always shown live state.
Somebody marking a thread resolved is worth as much as a comment saying
so, and it was the one thing the reviewer could miss. The feed counted
it; everything else counted comments. A thread whose comments had all
been read but which somebody had just resolved lit the feed's indicator
with nothing to show for it in the row, and could never be marked read
again - the read signal only arms itself while comments are unread, so
the indicator stayed lit for good.

One rule now: a thread's activity is what was said in it plus its
resolution, and whatever of that the reviewer has not seen is unread.
The thread's dot, the badges, the read signal and the main menu's
indicator all ask the same question, and unreadComments.js has become
unreadActivity.js to say so.

The count beside a badge no longer names what it counts - "1 unread"
rather than "1 unread comment" - since one of them may be a resolution.
tf added 2 commits August 27, 2026 15:29
The admin's indicator went by comments alone and only ever looked at
unresolved threads, so the one thing worth not missing was invisible
there: a thread somebody had just resolved dropped out of the query
along with its resolution. It counts now, beside the unread topics and
replies, and the indicator shows even where no topic is left open -
the last one being resolved is exactly the case.

The rule stays in step with the review interface: the resolution goes
by the thread's read mark, having none of its own, and the user's own
resolutions never count.
The line saying a thread was marked as resolved looked the same whether
the reviewer had seen that happen or not, which left the one event with
no comment to carry it silent inside the thread. It takes the colour the
dots and counts use when the resolution is what is unread - icon, wording
and resolver alike, since the whole line is the news.

Thread activity says which of its events is the resolution rather than
leaving it to be the one without a comment id.
@tf
tf merged commit c4fd710 into codevise:master Aug 27, 2026
18 checks passed
@tf
tf deleted the comments-feed branch August 27, 2026 13:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant