channels: sort rows on stable keys only — subscriber counts shifted

everything mid-load

Making subscribers the primary key reordered both rows and whole rows
as the per-channel subscriber refine streamed in: each background batch
re-sorted the screen while loading, and the list jumped several times on
entry (user: "the jumping is terrible"). The subscriber counts are not
in the video-listing fast path; they only arrive seconds later in
per-channel batches, so any ordering that leads with them cannot render
early.

Back to the stable keys — own row first, then video totals (then plays,
then subscribers) on both the row and the channel level. These come
entirely from the video listing before the first rebuild, so the screen
painted correctly once, verified: a single rebuild line across the whole
TV load, no further reshuffling. Video-less channels still never render,
the own row still holds first place whenever it exists.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jeena 2026-10-19 14:40:26 +00:00
parent b73958d617
commit d016805688

View file

@ -76,8 +76,7 @@ import net.jeena.jtubetv.ui.screens.list.dpadCenterActivatable
* Channels, SmartTube-style: one row per LOCAL user.
*
* - Row 1: the logged-in user ("<display name>'s Channels") — only when they own
* at least one channel WITH videos; then the other local users, rows and
* channels alike sorted by subscribers / videos / views (desc).
* at least one channel WITH videos; then the other local users by total videos.
* - Channels with no videos never render (watching-only accounts), at any level.
* - Followed-instance channels at the end, grouped per platform.
*
@ -281,10 +280,13 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() {
val byOwner = locals.groupBy { ownerKey(it) }
val groups = byOwner.values.map { row ->
val sorted = row.sortedWith(
// One ordering everywhere: subscribers, videos, views (desc).
compareByDescending<VideoChannelDto> { it.followersCount }
.thenByDescending { statOf(it) }
.thenByDescending { playsOf(it) },
// Stable keys only: all of these come from the video listing and
// never change mid-load — subscriber counts arrive later per-
// channel and must not reorder anything while loading (rows that
// jump while stats stream in read as broken).
compareByDescending<VideoChannelDto> { statOf(it) }
.thenByDescending { playsOf(it) }
.thenByDescending { it.followersCount },
)
val own = row.firstOrNull { it.name in ownNames }
UserGroup(
@ -296,9 +298,9 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() {
}
userRows = groups.sortedWith(
compareByDescending<UserGroup> { g -> g.channels.any { it.name in ownNames } }
.thenByDescending { g -> g.channels.sumOf { it.followersCount } }
.thenByDescending { g -> g.channels.sumOf { statsMap[it.name + "@" + it.host]?.videos ?: 0 } }
.thenByDescending { g -> g.channels.sumOf { statsMap[it.name + "@" + it.host]?.plays ?: 0 } },
.thenByDescending { g -> g.channels.sumOf { statsMap[it.name + "@" + it.host]?.plays ?: 0 } }
.thenByDescending { g -> g.channels.sumOf { it.followersCount } },
)
Log.w(
"JTube",