diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthPromptHost.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthPromptHost.kt index 8f45f7eb43..36edeebf68 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthPromptHost.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthPromptHost.kt @@ -149,7 +149,26 @@ private fun RelayAuthPromptDialog( // The purpose the user is most likely to recognize as "what I was just doing". val primary = remember(prompt) { prompt.purposes.primary() } - val faces = primary?.counterparties?.toList().orEmpty() + + // A thread names nobody by itself, so the sentence used to say "the rest of this conversation" + // about something the reader may not have on screen — these prompts routinely surface over the + // home feed or the settings screen. Resolving the notes to their authors turns the vague + // reference into a person the reader recognizes. + val threadFaces = + remember(primary) { + if (primary?.kind == AuthPurposeKind.THREAD) { + primary.notes.mapNotNullTo(LinkedHashSet()) { LocalCache.getNoteIfExists(it)?.author?.pubkeyHex }.toList() + } else { + emptyList() + } + } + + val faces = + primary + ?.counterparties + ?.toList() + .orEmpty() + .ifEmpty { threadFaces } val who = when (primary?.kind) { AuthPurposeKind.POST_VENUE, AuthPurposeKind.READ_VENUE -> @@ -380,7 +399,10 @@ private fun reasonFor( AuthPurposeKind.POST_VENUE -> stringRes(R.string.relay_auth_why_post_venue, who ?: "") AuthPurposeKind.READ_VENUE -> stringRes(R.string.relay_auth_why_read_venue, who ?: "") AuthPurposeKind.MY_INBOX -> stringRes(R.string.relay_auth_why_my_inbox) - AuthPurposeKind.THREAD -> stringRes(R.string.relay_auth_why_thread) + // Name the conversation by who is in it when we could resolve the notes; "this conversation" + // only survives as the fallback for a thread whose notes aren't in the cache yet. + AuthPurposeKind.THREAD -> + if (who.isNullOrBlank()) stringRes(R.string.relay_auth_why_thread) else stringRes(R.string.relay_auth_why_thread_with, who) // No attributable purpose. If it is the user's own relay we can at least say that much, // which is the only way MY_OWN_RELAY is ever reachable — the deriver is account-agnostic // (one shared socket, many accounts) so it cannot know whose relay this is. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/RelayAuthPurposeDeriver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/RelayAuthPurposeDeriver.kt index f044c521b4..d6bad70abc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/RelayAuthPurposeDeriver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/RelayAuthPurposeDeriver.kt @@ -89,6 +89,7 @@ object RelayAuthPurposeDeriver { val readAuthors = mutableSetOf() val readVenues = mutableSetOf() + val readThreadNotes = mutableSetOf() var readsMyInbox = false var readsThread = false var unattributedRead = false @@ -99,7 +100,14 @@ object RelayAuthPurposeDeriver { // Declared and self-contained: nobody else's identity is involved, so there is // nothing to collect — the flag alone drives the wording. AuthPurposeKind.MY_INBOX -> readsMyInbox = true - AuthPurposeKind.THREAD -> readsThread = true + // The notes are what makes the sentence nameable. The assembler either declares + // them (entityIds) or, as ReactionsFilterAssembler does, only puts them in the + // `e` tags — take whichever we get so the prompt can say whose conversation. + AuthPurposeKind.THREAD -> { + readsThread = true + explained?.entityIds?.let(readThreadNotes::addAll) + filter.tags?.get("e")?.let(readThreadNotes::addAll) + } // Declared, but the *who*/*what* still comes from the filter. Prefer the entity // ids the assembler named over sniffing tags, and fall back when it named none. AuthPurposeKind.READ_VENUE -> { @@ -135,7 +143,7 @@ object RelayAuthPurposeDeriver { if (readAuthors.isNotEmpty()) add(AuthPurpose(AuthPurposeKind.READ_OUTBOX, readAuthors)) if (readVenues.isNotEmpty()) add(AuthPurpose(AuthPurposeKind.READ_VENUE, venues = readVenues)) if (readsMyInbox) add(AuthPurpose(AuthPurposeKind.MY_INBOX)) - if (readsThread) add(AuthPurpose(AuthPurposeKind.THREAD)) + if (readsThread) add(AuthPurpose(AuthPurposeKind.THREAD, notes = readThreadNotes)) // Safety net: we're using this relay but couldn't say how — prompt rather than fail silently. if (isEmpty() && (unattributedWrite || unattributedRead)) add(AuthPurpose(AuthPurposeKind.OTHER)) } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 909decbeb2..6231d8129d 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1161,6 +1161,7 @@ It won\'t serve %1$s unless you log in. It won\'t serve your incoming replies, zaps and messages unless you log in. It won\'t serve the rest of this conversation unless you log in. + It won\'t serve the rest of your conversation with %1$s unless you log in. This is one of your own relays, and it asks everyone to log in. It asks everyone to log in. Amethyst can\'t tell what it\'s holding back. someone you haven\'t loaded yet diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayauth/AuthPurpose.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayauth/AuthPurpose.kt index cdf4166d4c..022ac62e03 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayauth/AuthPurpose.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayauth/AuthPurpose.kt @@ -75,6 +75,13 @@ data class AuthPurpose( val kind: AuthPurposeKind, val counterparties: Set = emptySet(), val venues: Set = emptySet(), + /** + * Event ids of the conversation being read, for [AuthPurposeKind.THREAD]. Kept separate from + * [venues] because a thread is not a room: these are note ids, and the only use for them is to + * resolve *whose* conversation it is at render time, so the prompt can say "your conversation + * with Alice" instead of "this conversation" about something the user may not have on screen. + */ + val notes: Set = emptySet(), ) /** The relay plus every live reason we currently have to auth with it. */