mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 08:47:33 +00:00
refactor(dm): unify per-relay reach vocabulary + clarify Nip04 routing name
Two naming families described the same concept — a relay's position in its
backward history walk — and the UI name overloaded the heavily-used "window"
and REQ "limit" terms. Collapse onto one vocabulary ("Reach"):
- RelayWindowLimit -> RelayReachCursor
- RelayWindowLimitMarkers -> RelayReachMarkers
- RelayWindowLimitSentinels -> RelayReachSentinels
And rename the NIP-04 per-relay routing map so it reads as a map, not a list:
- Nip04DmRelays (class) / nip04DMRelays (factory) -> Nip04DmRelayRouting / nip04DmRelayRouting
Pure rename — no behavior change. Design doc updated to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e9f2f1d7aa
commit
cd6537bfd5
@@ -124,12 +124,12 @@ to load, just not advancing) keeps it false.
|
||||
History paging is demand-driven by **per-relay window-limit markers** placed in
|
||||
the message stream, not by a scroll-position trigger:
|
||||
|
||||
- **`RelayWindowLimit`** — one per (protocol, relay): its `reachedUntil` depth,
|
||||
- **`RelayReachCursor`** — one per (protocol, relay): its `reachedUntil` depth,
|
||||
its `RelayReachState` (`REACHING ↓` / `STALLED …` / `DONE ✓`), and the
|
||||
`advance()` that pulls *that relay's* next page. Built in the feed views from
|
||||
each history manager's `relayProgress` map (gift wraps + NIP-04 combined; a
|
||||
protocol drops out of the list once `exhausted`).
|
||||
- **`RelayWindowLimitSentinels`** — the load *driver*, **hoisted above the
|
||||
- **`RelayReachSentinels`** — the load *driver*, **hoisted above the
|
||||
`LazyColumn`** (via `ChatFeedView`'s `sentinels` slot). Each non-done limit
|
||||
gets one stable effect (keyed by `protocol:url`) that watches `listState` and
|
||||
fires `advance()` when its gap is among the **currently visible rows** AND
|
||||
@@ -139,7 +139,7 @@ the message stream, not by a scroll-position trigger:
|
||||
hosting row, so any feed reorder (a live DM, a slow relay dribbling a page)
|
||||
tore the effect down and re-fired `advance()` on a static screen — re-arming
|
||||
stalled relays into a silence-watchdog storm. (commit `0394ec2a`)
|
||||
- **`RelayWindowLimitMarkers` / `RelayReachMarker`** — pure UI (via the
|
||||
- **`RelayReachMarkers` / `RelayReachMarker`** — pure UI (via the
|
||||
`markersInGap` slot): the "Relay sync: ✓ 8 · ↓ 1" divider at each relay's
|
||||
reached depth. Can be re-placed on every reorder without triggering paging.
|
||||
- **`BootstrapHistoryWhenEmpty`** — when the feed is genuinely `Empty` (the live
|
||||
@@ -148,7 +148,7 @@ the message stream, not by a scroll-position trigger:
|
||||
a time (debounced 1200ms, gated per loader on `!loading && !exhausted`) until
|
||||
messages appear and the markers take over, or the protocol exhausts.
|
||||
|
||||
### NIP-04 per-relay filter scoping (`Nip04DmRelays`)
|
||||
### NIP-04 per-relay filter scoping (`Nip04DmRelayRouting`)
|
||||
|
||||
A conversation's NIP-04 filters previously named the whole participant set on
|
||||
every relay, so a relay belonging to one correspondent was asked about all of
|
||||
@@ -156,7 +156,7 @@ them, and the `from-me` leg (`authors:[me]`) was sent to correspondents' inbox
|
||||
relays — which auth-walled relays reject outright ("all authors must be
|
||||
authenticated"), stalling the load.
|
||||
|
||||
`Nip04DmRelays` (in `FilterNip04DMs.kt`) is now two **per-relay key maps**
|
||||
`Nip04DmRelayRouting` (in `FilterNip04DMs.kt`) is now two **per-relay key maps**
|
||||
(`relay → which keys to name there`), built from the outbox model:
|
||||
|
||||
- **to me** (`#p:[me]`) — my inbox carries the whole group; each correspondent's
|
||||
@@ -244,13 +244,13 @@ source set (uses `java.util.concurrent`), visible to amethyst + desktop + quartz
|
||||
- `AccountGiftWrapsEoseManager.kt` (live tail) + `AccountGiftWrapsHistoryEoseManager.kt` (new, history).
|
||||
- `ChatroomNip04SubAssembler.kt` (live tail) + `ChatroomNip04HistorySubAssembler.kt` (new, history).
|
||||
- `ChatroomListNip04SubAssembler.kt` (live tail) + `ChatroomListNip04HistorySubAssembler.kt` (new, history).
|
||||
- `FilterNip04DMs.kt` (per-relay `Nip04DmRelays`, live + history builders), `FilterNip04DMsFromMe/ToMe.kt`, `FilterGiftWrapsToPubkey.kt` — `until`/`limit` added.
|
||||
- `FilterNip04DMs.kt` (per-relay `Nip04DmRelayRouting`, live + history builders), `FilterNip04DMsFromMe/ToMe.kt`, `FilterGiftWrapsToPubkey.kt` — `until`/`limit` added.
|
||||
- `AccountFilterAssembler`, `ChatroomFilterAssembler`, `ChatroomListFilterAssembler` — wire the new managers.
|
||||
|
||||
**Shared UI (commons, `commons/ui/feeds/`)** — extracted from amethyst so Android +
|
||||
Desktop (and any per-relay feed) render the same widgets; CMP `composeResources`
|
||||
strings, no app-theme / `java.time` deps.
|
||||
- `RelayReachMarker.kt` — `RelayWindowLimit` + sentinels (the hoisted, visibility-driven
|
||||
- `RelayReachMarker.kt` — `RelayReachCursor` + sentinels (the hoisted, visibility-driven
|
||||
paging driver) + markers (pure UI) + `RelayReachMarker`/`RelayReachState`.
|
||||
- `DmHistoryLoadingCard.kt` — the boundary status card + per-relay tap dialog +
|
||||
`historySubtitle`/`incompleteSubtitle`. Takes a `formatReachDate: (epochSeconds) -> String`
|
||||
|
||||
+9
-9
@@ -42,10 +42,10 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.DmHistoryLoadingCard
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachCursor
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachMarkers
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachSentinels
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayWindowLimit
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayWindowLimitMarkers
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayWindowLimitSentinels
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.resolveSharedMedia
|
||||
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
@@ -230,19 +230,19 @@ fun ChatroomViewUI(
|
||||
val user = accountViewModel.userProfile()
|
||||
|
||||
// Both protocols' per-relay window limits, each carrying the advance() that pulls its own next page.
|
||||
// Placed in the stream as sentinels (see RelayWindowLimitMarkers): a relay pages only while its
|
||||
// Placed in the stream as sentinels (see RelayReachMarkers): a relay pages only while its
|
||||
// marker is on screen, and keeps paging while it stays there. A protocol drops out once exhausted.
|
||||
val limits =
|
||||
remember(nip04Progress, giftWrapsProgress, nip04Exhausted, giftWrapsExhausted, user) {
|
||||
buildList {
|
||||
if (!giftWrapsExhausted) {
|
||||
giftWrapsProgress.forEach { (relay, p) ->
|
||||
add(RelayWindowLimit("17:${relay.url}", relayShortName(relay), p.reachedUntil, reachState(p)) { giftWrapsHistory.advance(user, relay) })
|
||||
add(RelayReachCursor("17:${relay.url}", relayShortName(relay), p.reachedUntil, reachState(p)) { giftWrapsHistory.advance(user, relay) })
|
||||
}
|
||||
}
|
||||
if (!nip04Exhausted) {
|
||||
nip04Progress.forEach { (relay, p) ->
|
||||
add(RelayWindowLimit("04:${relay.url}", relayShortName(relay), p.reachedUntil, reachState(p)) { nip04History.advance(relay) })
|
||||
add(RelayReachCursor("04:${relay.url}", relayShortName(relay), p.reachedUntil, reachState(p)) { nip04History.advance(relay) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,16 +284,16 @@ fun ChatroomViewUI(
|
||||
if (limits.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
{ newer, older -> RelayWindowLimitMarkers(limits, newer, older) }
|
||||
{ newer, older -> RelayReachMarkers(limits, newer, older) }
|
||||
},
|
||||
// The hoisted load driver that pulls each relay's next page while its marker is on screen,
|
||||
// off viewport visibility (see RelayWindowLimitSentinels) so feed reorders don't re-page.
|
||||
// off viewport visibility (see RelayReachSentinels) so feed reorders don't re-page.
|
||||
sentinels =
|
||||
if (limits.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
{ items, listState ->
|
||||
RelayWindowLimitSentinels(limits, listState) { index -> items.getOrNull(index)?.event?.createdAt }
|
||||
RelayReachSentinels(limits, listState) { index -> items.getOrNull(index)?.event?.createdAt }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
+3
-3
@@ -63,7 +63,7 @@ class ChatroomNip04HistorySubAssembler(
|
||||
private fun convoKey(key: ChatroomQueryState) = ConvoKey(user(key).pubkeyHex, key.room)
|
||||
|
||||
// The conversation's relay set for a key, resolved via the outbox model (per-relay scoped).
|
||||
private fun relaysFor(pk: ConvoKey): Collection<NormalizedRelayUrl>? = allKeys().firstOrNull { convoKey(it) == pk }?.let { nip04DMRelays(it.room.users, it.account)?.all }
|
||||
private fun relaysFor(pk: ConvoKey): Collection<NormalizedRelayUrl>? = allKeys().firstOrNull { convoKey(it) == pk }?.let { nip04DmRelayRouting(it.room.users, it.account)?.all }
|
||||
|
||||
private val pager = BackwardRelayPager<ConvoKey>("convo.nip04.history", relaysFor = ::relaysFor)
|
||||
|
||||
@@ -83,7 +83,7 @@ class ChatroomNip04HistorySubAssembler(
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter>? {
|
||||
val pk = convoKey(key)
|
||||
val relays = nip04DMRelays(key.room.users, key.account)
|
||||
val relays = nip04DmRelayRouting(key.room.users, key.account)
|
||||
if (!key.account.isWriteable() || relays == null) return emptyList()
|
||||
|
||||
// Only armed (advanced, not done) relays carry a REQ, each at its own requested cursor. A parked
|
||||
@@ -92,7 +92,7 @@ class ChatroomNip04HistorySubAssembler(
|
||||
if (armed.isEmpty()) return emptyList()
|
||||
DmRelayLog.log("convo.nip04.history", key.account)
|
||||
val scoped =
|
||||
Nip04DmRelays(
|
||||
Nip04DmRelayRouting(
|
||||
toMeRelays = relays.toMeRelays.filterKeys { it in armed },
|
||||
fromMeRelays = relays.fromMeRelays.filterKeys { it in armed },
|
||||
)
|
||||
|
||||
+6
-6
@@ -44,7 +44,7 @@ import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
* Scoping the key set per relay is what keeps us from sending, e.g., `authors=[bob]` to a relay that
|
||||
* is only charlie's — a filter that relay has no reason to serve.
|
||||
*/
|
||||
class Nip04DmRelays(
|
||||
class Nip04DmRelayRouting(
|
||||
val toMeRelays: Map<NormalizedRelayUrl, Set<HexKey>>,
|
||||
val fromMeRelays: Map<NormalizedRelayUrl, Set<HexKey>>,
|
||||
) {
|
||||
@@ -57,10 +57,10 @@ private fun addAll(
|
||||
keys: Collection<HexKey>,
|
||||
) = relays.forEach { map.getOrPut(it) { mutableSetOf() }.addAll(keys) }
|
||||
|
||||
fun nip04DMRelays(
|
||||
fun nip04DmRelayRouting(
|
||||
group: Set<HexKey>?,
|
||||
account: Account?,
|
||||
): Nip04DmRelays? {
|
||||
): Nip04DmRelayRouting? {
|
||||
if (group.isNullOrEmpty() || account == null) return null
|
||||
|
||||
val userOutboxRelays = account.homeRelays.flow.value
|
||||
@@ -98,7 +98,7 @@ fun nip04DMRelays(
|
||||
addAll(fromMe, inbox, listOf(it))
|
||||
}
|
||||
|
||||
return Nip04DmRelays(toMe, fromMe)
|
||||
return Nip04DmRelayRouting(toMe, fromMe)
|
||||
}
|
||||
|
||||
private fun toMeFilter(
|
||||
@@ -148,7 +148,7 @@ fun filterNip04DMs(
|
||||
windowStart: Long,
|
||||
): List<RelayBasedFilter>? {
|
||||
if (group.isNullOrEmpty() || account == null) return null
|
||||
val relays = nip04DMRelays(group, account) ?: return null
|
||||
val relays = nip04DmRelayRouting(group, account) ?: return null
|
||||
return relays.toMeRelays.map { (relay, authors) -> toMeFilter(relay, authors, account, since = windowStart, until = null, limit = null) } +
|
||||
relays.fromMeRelays.map { (relay, pTags) -> fromMeFilter(relay, pTags, account, since = windowStart, until = null, limit = null) }
|
||||
}
|
||||
@@ -160,7 +160,7 @@ fun filterNip04DMs(
|
||||
*/
|
||||
fun filterNip04DMsHistory(
|
||||
account: Account,
|
||||
relays: Nip04DmRelays,
|
||||
relays: Nip04DmRelayRouting,
|
||||
limit: Int,
|
||||
untilFor: (NormalizedRelayUrl) -> Long?,
|
||||
): List<RelayBasedFilter> =
|
||||
|
||||
+8
-8
@@ -40,10 +40,10 @@ import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.DmHistoryLoadingCard
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachCursor
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachMarkers
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachSentinels
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayWindowLimit
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayWindowLimitMarkers
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RelayWindowLimitSentinels
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
|
||||
@@ -185,7 +185,7 @@ private fun FeedLoaded(
|
||||
val oldestNip04Index = items.list.indexOfLast { it.event is PrivateDmEvent }
|
||||
|
||||
// Each relay's window limit, carrying the advance() that pulls its OWN next page. Placed in the list
|
||||
// at its reached depth as a sentinel (see RelayWindowLimitMarkers): a relay pages only while its
|
||||
// at its reached depth as a sentinel (see RelayReachMarkers): a relay pages only while its
|
||||
// marker is on screen and keeps paging while it stays there, so a spam-dense relay never floods —
|
||||
// you have to scroll through its messages to pull more. A protocol drops out once exhausted.
|
||||
val limits =
|
||||
@@ -193,12 +193,12 @@ private fun FeedLoaded(
|
||||
buildList {
|
||||
if (!giftWrapsExhausted) {
|
||||
giftWrapsProgress.forEach { (relay, p) ->
|
||||
add(RelayWindowLimit("17:${relay.url}", relayShortName(relay), p.reachedUntil, reachState(p)) { giftWrapsHistory.advance(user, relay) })
|
||||
add(RelayReachCursor("17:${relay.url}", relayShortName(relay), p.reachedUntil, reachState(p)) { giftWrapsHistory.advance(user, relay) })
|
||||
}
|
||||
}
|
||||
if (!nip04Exhausted) {
|
||||
nip04Progress.forEach { (relay, p) ->
|
||||
add(RelayWindowLimit("04:${relay.url}", relayShortName(relay), p.reachedUntil, reachState(p)) { nip04History.advance(user, relay) })
|
||||
add(RelayReachCursor("04:${relay.url}", relayShortName(relay), p.reachedUntil, reachState(p)) { nip04History.advance(user, relay) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,7 +206,7 @@ private fun FeedLoaded(
|
||||
|
||||
// Hoisted load driver: pulls each relay's next page off viewport visibility, so feed reorders
|
||||
// (a live DM bumping a room) no longer re-fire paging. The markers below are pure UI.
|
||||
RelayWindowLimitSentinels(limits, listState) { index -> items.list.getOrNull(index)?.createdAt() }
|
||||
RelayReachSentinels(limits, listState) { index -> items.list.getOrNull(index)?.createdAt() }
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = rememberFeedContentPadding(FeedPadding),
|
||||
@@ -240,7 +240,7 @@ private fun FeedLoaded(
|
||||
// Per-relay window-limit markers/sentinels belonging in the gap toward the next-older room:
|
||||
// each pulls its relay's next page while it's on screen. olderCreatedAt is null past the
|
||||
// oldest loaded room, so relays that have reached the bottom of the list sit there.
|
||||
RelayWindowLimitMarkers(
|
||||
RelayReachMarkers(
|
||||
limits,
|
||||
item.createdAt(),
|
||||
items.list.getOrNull(index + 1)?.createdAt(),
|
||||
|
||||
+12
-12
@@ -54,8 +54,8 @@ private val DividerThickness = 0.25.dp
|
||||
* True when [reachedUntil] falls in the gap between a newer message (at [newerCreatedAt]) and its
|
||||
* next-older neighbour (at [olderCreatedAt], null past the oldest end): the newer side is strictly
|
||||
* newer than the cursor and the older side is at or below it (or absent). This single predicate both
|
||||
* places the marker ([RelayWindowLimitMarkers]) and decides when its paging sentinel is on screen
|
||||
* ([RelayWindowLimitSentinels]), so the two can never disagree about which gap a cursor lives in.
|
||||
* places the marker ([RelayReachMarkers]) and decides when its paging sentinel is on screen
|
||||
* ([RelayReachSentinels]), so the two can never disagree about which gap a cursor lives in.
|
||||
*/
|
||||
internal fun reachedFallsInGap(
|
||||
reachedUntil: Long,
|
||||
@@ -87,12 +87,12 @@ data class RelayReach(
|
||||
/**
|
||||
* One relay's window-limit: places a marker and carries the [advance] that pulls that relay's next,
|
||||
* older page. The marker sits at [reachedUntil] (the oldest point the relay has paged to);
|
||||
* [RelayWindowLimitMarkers] draws it and [RelayWindowLimitSentinels] fires [advance] while it is on
|
||||
* [RelayReachMarkers] draws it and [RelayReachSentinels] fires [advance] while it is on
|
||||
* screen.
|
||||
*
|
||||
* @param key stable identity (protocol tag + relay url) so the sentinel survives list reorders.
|
||||
*/
|
||||
data class RelayWindowLimit(
|
||||
data class RelayReachCursor(
|
||||
val key: String,
|
||||
val name: String,
|
||||
val reachedUntil: Long,
|
||||
@@ -103,7 +103,7 @@ data class RelayWindowLimit(
|
||||
/**
|
||||
* Drives demand-driven paging for every limit, **hoisted above the list** so its identity does not ride
|
||||
* on which row currently hosts the marker. Each non-done limit gets one stable effect (keyed by
|
||||
* [RelayWindowLimit.key]) that watches the [listState] and pulls that relay's next page when its marker
|
||||
* [RelayReachCursor.key]) that watches the [listState] and pulls that relay's next page when its marker
|
||||
* is on screen.
|
||||
*
|
||||
* Why hoisted: the marker for a limit lives in exactly one gap (between the two rows straddling its
|
||||
@@ -120,11 +120,11 @@ data class RelayWindowLimit(
|
||||
* A done relay drives nothing.
|
||||
*
|
||||
* @param createdAtAt createdAt of the list item at an index (null past the ends / for non-message rows),
|
||||
* so the visible-gap test mirrors [RelayWindowLimitMarkers]'s placement against only the on-screen rows.
|
||||
* so the visible-gap test mirrors [RelayReachMarkers]'s placement against only the on-screen rows.
|
||||
*/
|
||||
@Composable
|
||||
fun RelayWindowLimitSentinels(
|
||||
limits: List<RelayWindowLimit>,
|
||||
fun RelayReachSentinels(
|
||||
limits: List<RelayReachCursor>,
|
||||
listState: LazyListState,
|
||||
createdAtAt: (index: Int) -> Long?,
|
||||
) {
|
||||
@@ -139,7 +139,7 @@ fun RelayWindowLimitSentinels(
|
||||
val r = reached.value
|
||||
val at = getAt.value
|
||||
// Visible if any on-screen row is the "newer" side of the gap holding this cursor —
|
||||
// the same predicate RelayWindowLimitMarkers uses to place the marker, but over the
|
||||
// the same predicate RelayReachMarkers uses to place the marker, but over the
|
||||
// visible rows only.
|
||||
val onScreen =
|
||||
listState.layoutInfo.visibleItemsInfo.any { info ->
|
||||
@@ -165,12 +165,12 @@ fun RelayWindowLimitSentinels(
|
||||
/**
|
||||
* Renders the window-limit markers for the relays whose limit falls in the gap between a newer message
|
||||
* (at [newerCreatedAt]) and its next-older neighbour (at [olderCreatedAt], null at the oldest end). Pure
|
||||
* UI: the load driving lives in [RelayWindowLimitSentinels], so this can be (re)placed freely per row on
|
||||
* UI: the load driving lives in [RelayReachSentinels], so this can be (re)placed freely per row on
|
||||
* every feed reorder without triggering any paging.
|
||||
*/
|
||||
@Composable
|
||||
fun RelayWindowLimitMarkers(
|
||||
limits: List<RelayWindowLimit>,
|
||||
fun RelayReachMarkers(
|
||||
limits: List<RelayReachCursor>,
|
||||
newerCreatedAt: Long?,
|
||||
olderCreatedAt: Long?,
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user