feat: every list of games follows the reader's order, dailies included
The focus bar, the settings list, the timeline's lanes and the dailies strip all go through `orderGames` now, resolved once in App. `games` itself stays in feed order on purpose: `adoptNewLanes` diffs it and `knownGames` is seeded from it, so ordering it at source would let a display preference reach the code that decides which of a reader's games get hidden — a reordering bug would become a game-silently-switched-off bug. The dailies strip also groups. It was `[...chores, ...repeating]`, which put Genshin's commissions and Genshin's own login event at opposite ends with a dozen games between them; a game's chore and its repeating events are now adjacent, chore first, events in the order they arrived. Collapsed that is adjacency alone — no per-game headings, because this is the part of the page answerable in ten seconds and a heading each would make it the tallest block on it, pushing "next to expire" down the page. Two things moved to where they belong. Skipping a standing chore for a lane the reader invented is `dailyGroups`' rule, not the call site's: filtering those lanes out in App also cost a reader's own game its place in the order its events group under. And the expanded catch-up panel is its own exported component, so which days it offers and whose clock they were cut on are testable rather than trapped behind a `useState`. `Welcome` drops its own comparator for the shared rule — the picker runs before any stored order, so it asks for the A–Z case and cannot drift from the four surfaces behind it. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
bf7959c916
commit
721c4c90fb
@@ -74,11 +74,21 @@ export interface Lane<T> {
|
||||
* modes, lane mode included: leaving a lane's given order alone there would
|
||||
* produce the block it was told not to draw, minus the heading that explained
|
||||
* it, which is the worst of both answers.
|
||||
*
|
||||
* `gameOrder` stacks the lanes themselves — the reader's own game order, so
|
||||
* their main game is the top lane instead of whichever one held the first row.
|
||||
* It orders **lanes and never the rows inside one**, which is the same rule as
|
||||
* above read one level up. A game the order does not name sorts after the ones
|
||||
* it does, in the order its rows arrived, so this stays total for a lane the
|
||||
* reader never placed. Omitted leaves the stacking exactly as it was.
|
||||
*
|
||||
* The merged mode ignores it, having one lane and no game to order by.
|
||||
*/
|
||||
export function timelineLanes<T extends Row>(
|
||||
rows: readonly T[],
|
||||
mode: TimelineGroup,
|
||||
split = true,
|
||||
gameOrder?: readonly LaneId[],
|
||||
): Array<Lane<T>> {
|
||||
const order = split ? endingSoonestFirst : byDeadline;
|
||||
|
||||
@@ -91,7 +101,20 @@ export function timelineLanes<T extends Row>(
|
||||
for (const row of rows) {
|
||||
byGame.set(row.event.game, [...(byGame.get(row.event.game) ?? []), row]);
|
||||
}
|
||||
return [...byGame].map(([game, laneRows]) => ({
|
||||
|
||||
let lanes = [...byGame];
|
||||
if (gameOrder !== undefined) {
|
||||
// Unplaced lanes take a rank past every placed one, and ties keep their
|
||||
// arrival order — `sort` is stable, so a game the reader never placed does
|
||||
// not jump the ones they did.
|
||||
const rank = (game: LaneId) => {
|
||||
const at = gameOrder.indexOf(game);
|
||||
return at === -1 ? gameOrder.length : at;
|
||||
};
|
||||
lanes = lanes.sort(([a], [b]) => rank(a) - rank(b));
|
||||
}
|
||||
|
||||
return lanes.map(([game, laneRows]) => ({
|
||||
id: game,
|
||||
game,
|
||||
rows: split ? laneRows : [...laneRows].sort(byDeadline),
|
||||
|
||||
Reference in New Issue
Block a user