Fix DM thread paging: scope the direction filter under the cursor #3

Closed
vinz wants to merge 0 commits from fix/dm-thread-paging-precedence into hever-next
Owner

What

The direct-message thread endpoint could make a client scroll forever with just a spinner when loading older messages.

Cause

All three branches of DirectMessageController::thread() filter the two directions of a conversation with a top-level ->where(...)->orWhere(...):

->where("id", "<", $max_id)
->where(fn ($q) => $q->where("from_id", $pid)->where("to_id", $uid))
->orWhere(fn ($q) => $q->where("from_id", $uid)->where("to_id", $pid))

which SQL reads as (id < max_id AND their→you) OR (you→them). The orWhere escapes the cursor, so the requesting user’s own sent messages ignore max_id and come back in full on every page. A client paging back through a thread with ≥8 sent messages receives the same 8 each call, dedupes them to zero new, never advances the cursor, and spins indefinitely.

Verified live against scatto.social: max_id=52 returned messages 124,121,120,52,51 — the 124/121/120 are own-messages newer than the cursor, bypassing it.

Fix

Wrap the two-direction test in a single nested closure so the cursor scopes the whole thing: id < max_id AND ((their→you) OR (you→them)). Applied to both the max_id (older) and min_id (newer) branches. The no-cursor else branch has no id filter to escape and is left unchanged.

Client side

Scatto 2.9.6 also gained a guard (a page that adds nothing new ends paging) so it survives an unfixed server without the infinite spinner, but that only masks the symptom — this is the actual fix, and restores full older-message paging.

## What The direct-message thread endpoint could make a client scroll forever with just a spinner when loading older messages. ## Cause All three branches of `DirectMessageController::thread()` filter the two directions of a conversation with a top-level `->where(...)->orWhere(...)`: ```php ->where("id", "<", $max_id) ->where(fn ($q) => $q->where("from_id", $pid)->where("to_id", $uid)) ->orWhere(fn ($q) => $q->where("from_id", $uid)->where("to_id", $pid)) ``` which SQL reads as `(id < max_id AND their→you) OR (you→them)`. The `orWhere` escapes the cursor, so the requesting user’s **own sent messages ignore `max_id`** and come back in full on every page. A client paging back through a thread with ≥8 sent messages receives the same 8 each call, dedupes them to zero new, never advances the cursor, and spins indefinitely. Verified live against scatto.social: `max_id=52` returned messages `124,121,120,52,51` — the `124/121/120` are own-messages newer than the cursor, bypassing it. ## Fix Wrap the two-direction test in a single nested closure so the cursor scopes the whole thing: `id < max_id AND ((their→you) OR (you→them))`. Applied to both the `max_id` (older) and `min_id` (newer) branches. The no-cursor `else` branch has no `id` filter to escape and is left unchanged. ## Client side Scatto 2.9.6 also gained a guard (a page that adds nothing new ends paging) so it survives an unfixed server without the infinite spinner, but that only masks the symptom — this is the actual fix, and restores full older-message paging.
The thread query filtered the two directions of a conversation with a
top-level where()/orWhere(), which reads as

  (id < max_id AND their->you) OR (you->them)

so the orWhere escaped the cursor constraint and the sender own messages
were returned on every page regardless of max_id. A client paging back
through a thread with eight or more sent messages received the same eight
each time, made no progress, and span forever. Both the max_id (older) and
min_id (newer) branches are affected; the no-cursor branch has no id filter
to escape and is unchanged.

Wrapping the two-direction test in a single nested closure scopes it under
the cursor: id < max_id AND ((their->you) OR (you->them)).
vinz closed this pull request 2026-09-18 17:48:13 +02:00

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
vinz/pixelfed-scatto!3
No description provided.