Commit Graph

12326 Commits

Author SHA1 Message Date
Claude ad423ead29 fix(nests): keep presence out of channel chat + render kind:10312
Two-part fix for kind-10312 [MeetingRoomPresenceEvent] showing up as
empty cards in the channel chat panel.

(1) Filter at the chat-feed level. ChannelFeedFilter now rejects
MeetingRoomPresenceEvent — these only land in `channel.notes` so the
home live-bubble (HomeLiveFilter) can detect a follow broadcasting
in a Nest by walking channel.notes. They aren't chat content; the
chat panel rendering them as empty rows was a leak from that
home-bubble plumbing.

The filter is in ChannelFeedFilter rather than upstream in
LocalCache.consume because removing presence from channel.notes
would silently break the home-bubble's broadcasting-now detection.
This way the channel keeps the events for non-chat consumers and
the chat just refuses to surface them.

(2) Renderer fallback. NoteCompose now dispatches kind-10312 to
RenderMeetingRoomPresence — a one-line italic status row ("@user ·
raised their hand" / "stepped on stage" / "is speaking" / "joined
as audience" / "left the nest"). After (1) this only shows up on
non-chat surfaces (search results, thread view of a quoted
presence, profile timeline, …), but renders informatively instead
of as the empty card it was before.

Five new strings under nest_presence_*.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
2026-04-27 18:05:02 +00:00
Claude aadf43d5ae Merge remote-tracking branch 'origin/main' into claude/audio-rooms-android-ui-PIpFN 2026-04-27 17:57:03 +00:00
Claude a826766808 fix(nests): hide kind-30312 events missing the EGG-01 required tags
NestsFeedFilter rendered any kind:30312 the relay handed us, including
shells with only a `d` and `a` tag (no room name, no status, no
service / endpoint). The lobby card then drew an empty title row with
a Join button that would 410 unknown_room — confusing UX.

Add a feed-filter gate: a kind:30312 must carry all four of EGG-01
rule 2's required tags before it shows up:

  - room      (display name)
  - status    (any recognized value, including the "live" / "ended"
               legacy aliases nostrnests-web emits)
  - service   (auth sidecar URL)
  - endpoint  (MoQ relay URL)

Closed rooms that DO have all four still render — they may carry a
`recording` tag (EGG-11) and the listen-back affordance is the only
path to that audio post-close.

Sub-rooms (kind:30313 MeetingRoomEvent) get a parallel gate — must
carry a non-blank title.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
2026-04-27 17:53:25 +00:00
Vitor Pamplona 09920ac8d1 Merge pull request #2603 from vitorpamplona/claude/fix-bottom-nav-back-arrow-oXTpX
Add back button visibility check to prevent navigation from root
2026-04-27 13:53:17 -04:00
Claude 1bb9779e9e feat(nests): paint themed background color on the room screen
NestThemedScope was overriding MaterialTheme.colorScheme but the Box
holding the room body never explicitly painted the themed background
color — only consumers that read MaterialTheme.colorScheme.background
themselves picked up the change. A room that shipped
`["c", hex, "background"]` (EGG-10) but no `bg` image therefore
fell back to the platform surface and never looked themed.

Add `.background(themed.background)` to the Box. The optional `bg`
image still paints on top (per EGG-10 rule 3 — image overlays the
color); when no image is present, the themed color tints the whole
screen, matching the lobby card's containerColor pattern from the
last commit.

PIP screen left as-is: it's a small system-managed floating window
where the platform surface color is the right call.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
2026-04-27 17:49:55 +00:00
Claude 76d7753a34 feat(nests): richer NestJoinCard — cover, theme, host, speakers
Replaces the title-and-summary stub with a proper lobby card.

Render order top-to-bottom:
  1. Cover image (16:9, ContentScale.Crop) when the kind:30312 ships
     an `image` tag. Skipped silently when absent.
  2. Title + status flag row — reuses the existing
     MeetingSpaceOpenFlag / MeetingSpacePrivateFlag /
     MeetingSpaceClosedFlag / MeetingSpacePlannedFlag composables, so
     LIVE/PRIVATE/CLOSED/PLANNED badges visually match the in-feed
     RenderMeetingSpaceEvent surface.
  3. Summary, capped at 3 lines with ellipsis.
  4. Host row: 36dp avatar + UsernameDisplay + "Host" label. Avatar is
     tappable → profile route via the `nav` parameter.
  5. Speaker row: up to 5 24dp avatars from the kind:30312 `p`-tags
     (role=speaker | admin), with a "+N" overflow label. Each avatar
     navigates to its profile.
  6. "Join nest" button bottom-right.

Theming applies the room's color tags (EGG-10):
  - `["c", hex, "background"]` → Card containerColor
  - `["c", hex, "text"]` → all body text colors
  - `["c", hex, "primary"]` → JoinNestButton containerColor (passed
    via new optional `primaryColorOverride` parameter so the in-feed
    JoinNestButton call from RenderMeetingSpaceEvent stays unchanged)

Background image (`bg` tag) is intentionally NOT applied at the lobby
card level — it's reserved for the in-room screen via NestThemedScope
(which does need the full-bleed shader-based tile mode). Painting it
on a card would compete with the cover.

API change: NestJoinCard now takes `nav: INav`. The single call site
in ChannelView already has nav in scope.

New string: nest_lobby_host_label = "Host".

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
2026-04-27 17:32:51 +00:00
Claude dd926e5d51 fix(nav): hide back arrow when screen is the bottom of the back stack
Bottom-nav taps clear the stack with popUpTo(route) { inclusive = true },
so a back arrow on those screens has nothing to pop. Switch the back-arrow
visibility to a runtime check on INav.canPop() — true when the destination
has a previous back-stack entry (drawer or any deep push), false when it's
the root (bottom-nav entry).

Applied across:
- TopBarWithBackButton: now takes nav: INav and renders the icon only
  when nav.canPop()
- UserDrawerSearchTopBar: shows back arrow when canPop, drawer opener
  otherwise (covers Home, Messages, Video, Discover, Notifications,
  Communities, Articles, Pictures, Shorts, PublicChats, FollowPacks,
  LiveStreams, AudioRooms, Longs, Polls, Badges, Products,
  BrowseEmojiSets)
- WebBookmarks, Drafts, Wallet: wrap custom navigationIcon in canPop
- ProfileHeader: floating back arrow on the banner when canPop
2026-04-27 17:29:27 +00:00
Claude 76e9a146f9 feat(nests): per-avatar mic & hand badges + leave-stage button
Brings ParticipantsGrid and TalkRow closer to the nostrnests web UI
without restructuring the screen.

ParticipantsGrid:
  - HandRaiseBadge: yellow circle with PanTool glyph at the top-right
    of every member with handRaised=true. Subtle vertical bounce loop
    via rememberInfiniteTransition (mirrors nostrnests'
    animate-bounce). Shows in BOTH on-stage and audience sections —
    the audience hand-up is the queue.
  - MicStateBadge: bottom-center pill on on-stage speakers who are
    publishing. Color encodes:
      green   — speaking now (in speakingNow)
      red     — publishing but muted (presence.muted=1)
      primary — publishing, mic open, currently silent
    Skipped for audience (showMicBadge=false on the audience section)
    and for on-stage members who aren't publishing.

TalkRow:
  - "Leave stage" OutlinedButton added in both Idle and Broadcasting
    states. Drops the user off-stage via NestViewModel.setOnStage(false)
    (presence emits onstage="0" → ParticipantGrid drops them to the
    audience section). The Broadcasting variant also calls
    stopBroadcast() so the audio session ends with the role change.
  - "Stop talking" stays for users who want to pause audio without
    leaving the stage.

New string: nest_leave_stage = "Leave stage".

Colors are hardcoded (yellow-500 / green-500) to match nostrnests
exactly — they read consistently in dark and light modes against the
participant tile background. Theming hook is a follow-up.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
2026-04-27 17:25:39 +00:00
Vitor Pamplona 6f6bbb371e Merge pull request #2602 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-27 13:19:58 -04:00
Claude e0462bca61 Merge remote-tracking branch 'origin/main' into claude/audio-rooms-android-ui-PIpFN 2026-04-27 15:52:35 +00:00
Crowdin Bot 8ad5b97314 New Crowdin translations by GitHub Action 2026-04-27 15:52:13 +00:00
Vitor Pamplona 4c1ff98296 Merge pull request #2600 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-27 11:50:49 -04:00
Vitor Pamplona aad5a0e13f Merge pull request #2601 from vitorpamplona/claude/fix-channel-key-type-f5wU6
Make ChatroomLazyKey types Serializable for LazyColumn state
2026-04-27 11:50:42 -04:00
Claude 4c6d6eb4b4 perf(chats): hold Set in PrivateChatLazyKey instead of joined String
The previous fix sorted + joined the user pubkeys into a comma-separated
String to keep the key Bundle-storable. That brought back the
StringBuilder + char[] + String + sorted-List allocation per visible
chatroom that the typed-key rework had eliminated.

ChatroomKey.users is often a kotlinx PersistentOrderedSet, which isn't
java.io.Serializable, so we can't just hold the Set reference as-is.
Copying into a HashSet costs one HashMap allocation per key, much less
than the joined-String path, and Set equality stays order-independent so
two equivalent chatrooms still hash to the same bucket.
2026-04-27 15:47:19 +00:00
Claude 298fcb7185 style: spotlessApply 2026-04-27 15:36:34 +00:00
Crowdin Bot 1992fa7476 New Crowdin translations by GitHub Action 2026-04-27 15:32:52 +00:00
Vitor Pamplona af6b8c8a52 Removes old MLKit labeler because of the terrible labels it was creating. 2026-04-27 11:29:28 -04:00
Claude 6714e74c76 Merge remote-tracking branch 'origin/main' into claude/audio-rooms-android-ui-PIpFN 2026-04-27 15:27:39 +00:00
Claude d9ba7187ca fix(chats): make ChatroomLazyKey Bundle-storable
LazyColumn keys are persisted in a SaveableStateHolder, which on Android
must be Bundle-storable (primitives, Parcelable, or Serializable). The
recent perf rework wrapped keys in plain data classes, which Kotlin does
not auto-mark Serializable, so opening a public chat crashed with:

  java.lang.IllegalArgumentException: Type of the key
    PublicChannelLazyKey(channelId=...) is not supported.

Mark the sealed interface Serializable, and decompose RoomId/ChatroomKey
fields (which live in quartz commonMain and can't depend on the JVM-only
Serializable interface) into String primitives. Each variant still wraps
a stable per-chatroom identity, so reorders move rows instead of
recreating them.
2026-04-27 15:17:51 +00:00
Claude 462a1c446c feat(nests): rebuild chat UI on NestViewModel.chat with LiveStream renderer
Replaces the v2 ChannelFeedViewModel/ChannelNewMessageViewModel detour
(commit bdb82f0) with a chat panel that:

  - Reads messages from `NestViewModel.chat` directly (single source of
    truth for the room — same VM that holds presence, reactions, and
    hand-raise queue),
  - Renders each message via `ChatroomMessageCompose` — the exact same
    per-row composable that LiveStream chat uses in
    `ChatFeedLoaded` — so visual rendering (avatars, names,
    timestamps, NIP-21 mentions, embedded media, link previews,
    reactions count) matches the rest of Amethyst's chat surfaces,
  - Uses a slim composer reusing `ThinPaddingTextField` +
    `ThinSendButton` (the same components inside `EditFieldRow`),
  - LazyColumn with `reverseLayout=true` and the message list reversed,
    so newest is at the bottom and auto-scroll works naturally.

Sends route through `accountViewModel.account.signAndComputeBroadcast`
of `LiveActivitiesChatMessageEvent.message(text, roomATag)` — same wire
path the room subscription is already listening on.

Composer is intentionally slim for v1: text-only. Drafts, @-mention
picker, file attachments, and reply preview are pinned to
ChannelNewMessageViewModel and out of scope here. Adding any of them is
a follow-up that can either (a) port the relevant primitives onto
NestViewModel or (b) widen NestViewModel to expose those fields.

New: `BouncingIntentNav` — Activity-context INav that translates the
small set of nav requests a chat row generates (Profile / Note /
Hashtag / EventRedirect / LiveActivityChannel / Community /
PublicChatChannel) into `nostr:` URIs and bounces them as ACTION_VIEW
Intents at MainActivity. AppNavigation already routes intent.data
through `uriToRoute`, so destinations land in the main app's NavHost
without receiver-side changes. NestActivity stays running in its own
task while the user explores; back from MainActivity returns to the
room with audio uninterrupted (foreground service).

Routes that don't have a `nostr:` mapping (settings, drafts, private
chatrooms, etc.) are no-ops in the bouncing nav — those aren't
reachable from a chat-row tap anyway.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
2026-04-27 15:16:36 +00:00
Vitor Pamplona 54ef9ead2d Merge pull request #2599 from vitorpamplona/claude/remove-pause-button-2btEI
Remove stop action from notification service and update icon imports
2026-04-27 10:52:37 -04:00
Claude bdb82f0631 feat(nests): migrate chat panel to LiveStream chat stack
The hand-rolled NestChatPanel — bare LazyColumn of avatar + name +
plain-text rows + an OutlinedTextField composer — is replaced by the
same `RefreshingChatroomFeedView` + `EditFieldRow` stack that
`LiveActivityChannelView` uses for kind:30311 streaming chats.

The kind-30312 address already registers a `LiveActivitiesChannel`
inside `LocalCache.liveChatChannels` (via
`LocalCache.consume(LiveActivitiesChatMessageEvent)`), so handing the
channel to `ChannelFeedViewModel.Factory` is enough — no kind-30311-
specific path needed. The kind-1311 relay subscription stays where it
is in `NestActivityContent.RoomChatFilterAssemblerSubscription`, which
populates `channel.notes` exactly the same way.

Net effect for the user: chat now renders with the rest of Amethyst's
chat features — NIP-21 mention rendering, embedded images / videos,
reply previews, draft handling, mention/file pickers, replies — instead
of plain bodies in a 220dp box.

Layout intentionally unchanged elsewhere: the chat panel keeps its
embedded place at the bottom of NestFullScreen's verticalScroll Column
(top section is good per user). The chat list gets a fixed 420dp height
because a vertically-scrollable LazyColumn child cannot share its
parent's verticalScroll.

Nav: the activity has no Compose nav graph, so an `EmptyNav()` is
passed. Visual rendering is correct; in-message tap navigation (profile
tap, embedded note open, …) is a no-op until we plumb a deep-link-
bouncing INav. Outside scope for this commit.

`NestViewModel.chat` StateFlow is now unused by the panel (kept for now
to avoid touching the VM in the same change; cleanup is a follow-up).

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
2026-04-27 14:47:30 +00:00
Claude aa7b5b054f fix(uploads): migrate ImageVideoDescription icons to MaterialSymbols
The AI-suggested alt-text chip was using androidx.compose.material.icons.*,
which the project no longer pulls in (migrated to MaterialSymbols a while
back). The file failed to compile until the dep was either re-added or
the icons migrated. Switching to MaterialSymbols.AutoAwesome / .Close.
2026-04-27 14:46:20 +00:00
Claude c13d844416 feat(threadview): render Nest cards in NoteMaster
When the thread-screen master note is a kind:30312 MeetingSpaceEvent
(Nest) or kind:30313 MeetingRoomEvent, route through
RenderMeetingSpaceEvent / RenderMeetingRoomEvent — same path
NoteCompose uses for non-master entries (NoteCompose.kt:1034-1040).

Without this, the master note in the ThreadFeedView's FullBleedNoteCompose
fell through to the generic note body and showed neither the Nest card
nor the Join CTA. Users who landed on a Nest via search, quote, or naddr
deep-link saw a blank-looking thread; tapping the in-feed card now (since
the routing fix in 98202b6) opens NestActivity directly, but the thread
view itself was the remaining gap.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
2026-04-27 14:37:08 +00:00
Claude dc3ac31ae4 refactor: rename Audio Room → Nest project-wide
Aligns class names, package paths, string resource keys, UI text and
intent actions with the Nests branding used by the EGG specs in
nestsClient/specs/. Mechanical rename — no behavior change.

- Folders: audiorooms/ → nests/ (5 paths across amethyst, quartz)
- 30+ class renames (AudioRoom* → Nest*, AudioRooms* → Nests*)
- String resource keys audio_room_* → nest_*
- UI strings "Audio Room"/"Audio Rooms" → "Nest"/"Nests" (incl. all locales)
- Intent extras AUDIO_ROOM_* → NEST_*
- Compose route Route.AudioRooms → Route.Nests

Spec-aligned identifiers (MeetingSpaceEvent, meetingSpaces/, the nip53*
packages) are intentionally untouched — those are NIP-53 protocol
names, not "audio room" branding.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
2026-04-27 14:36:50 +00:00
Claude 55875b060e fix(notifications): remove broken Pause action from always-on service
The Pause action called stopSelf(), but onDestroy() then triggered the
auto-restart broadcast (because alwaysOnNotificationService was still
enabled), so the notification reappeared seconds later. There was no way
to actually pause without toggling the setting, so the button was just
confusing.

Drops the ACTION_STOP intent, the notification action, and the
always_on_notif_stop string from all locales. Also includes incidental
spotless fixes the pre-commit hook required.
2026-04-27 14:32:55 +00:00
Claude 208c90b246 feat(audiorooms): host-leave confirmation dialog
When the host taps "Leave" we now show a 3-button confirmation:
  - Close room  → re-publish kind:30312 with status="closed", then leave
  - Just leave  → leave; room stays open until the 8h staleness window
  - Cancel      → dismiss

Audience-side leave flow unchanged. Without this prompt, hosts who tap
Leave silently abandoned the room — listeners saw it as "live" with no
audio until the EGG-01 rule 7 staleness timeout, eight hours later.

Implementation:
  - `closeMeetingSpace(accountViewModel, event)` builds a verbatim
    FormState from the event and reuses
    EditAudioRoomViewModel.buildEditTemplate(... STATUS.CLOSED). Bypasses
    the VM's mutable state so unsaved edits in an open Edit sheet can't
    leak into the close payload.
  - On publish failure we still leave (the user asked to) and surface a
    toast — the room will auto-close at the 8h timeout.

Process death (host backgrounded + Android-killed) is still handled by
the same 8h fallback; no extra wire changes.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
2026-04-27 14:24:01 +00:00
Claude addb2a4abb fix(quartz): accept legacy nostrnests tag names on kind-30312 read
The first-party nostrnests web client (NestsUI-v2) emits kind-30312
events using NIP-53 *streaming-event* tag names rather than the
kind-30312 names defined by NIP-53 itself. Confirmed against
`nostrnests/nests/NestsUI-v2/src/components/EditRoomDialog.tsx` and
`useRoomList.ts`:

  - room name: `title`     (NIP-53 spec for 30312: `room`)
  - MoQ relay: `streaming` (NIP-53 spec for 30312: `endpoint`)
  - moq-auth:  `auth`      (NIP-53 spec for 30312: `service`)
  - status:    `live`      (NIP-53 spec for 30312: `open` / `private`
                            / `closed` / `planned`)

Our parsers followed the NIP-53 spec, so events from the production
nostrnests web app fell through to status=null, which `checkStatus`
then surfaced as "Ended" in the live-rooms UI. Add legacy-alias
acceptance to all four parsers — read-side only; we still emit the
canonical NIP-53 forms, matching EGG-01.

Per-tag changes:
  StatusTag:      "live" → OPEN, "ended" → CLOSED
  RoomNameTag:    "title" alias → room
  EndpointUrlTag: "streaming" alias → endpoint
  ServiceUrlTag:  "auth" alias → service

No emit-side changes; no spec change.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
2026-04-27 14:12:23 +00:00
Claude eae28ab943 feat(audiorooms): prompt to add default server when user has none
When the + FAB is tapped on Audio Rooms and the user's kind:10112
nests-server list is empty (or has no http(s) entries), show an
AlertDialog offering to add the built-in default
(CreateAudioRoomViewModel.DEFAULT_SERVICE_URL — moq.nostrnests.com)
to their list, then continue into the create-room sheet.

Wired through the existing Account.sendNestsServersList path used by
the Settings screen's NestsServersViewModel.save(), so the resulting
kind:10112 propagates to the user's outbox identically. On signer/
network failure surface a toast and keep the dialog dismissed (the
user can retry from Settings).

If the user already has a usable server, behavior is unchanged — the
FAB opens the create-room sheet directly.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
2026-04-27 14:04:31 +00:00
Vitor Pamplona c31805c227 Merge pull request #2598 from vitorpamplona/claude/add-image-labeling-Yf6vg
Add AI-powered alt-text suggestions for images
2026-04-27 10:02:03 -04:00
Claude 98202b63bc fix(audiorooms): tap on rooms-feed card opens room, not thread view
ChannelCardCompose wraps content in ClickableNote, whose default onClick
calls routeFor(note, account). For MeetingSpaceEvent (kind:30312) that
falls through routeForInner's `is AddressableEvent` branch
(RouteMaker.kt:159-161) and returns Route.Note(addressTag) — the thread
view, which is the bug.

Bypass ChannelCardCompose for the rooms feed: AudioRoomFeedCard
renders RenderLiveActivityThumb directly inside a Column.clickable that
launches AudioRoomActivity, mirroring the home live-bubble pattern in
RenderLiveActivityBubble.kt:70-95. Falls back to the thread route when
service/endpoint/d are missing, same as the bubble.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
2026-04-27 14:00:11 +00:00
Claude f2c58adcf1 perf(ai): cache feature status, downscale bitmaps, lazy-init clients
- Cache the AICore FeatureStatus check per service instance — was
  re-running an RPC on every image attach.
- Two-pass decode with inSampleSize so 12 MP camera shots become
  ~1024 px before we hand them to the describer (avoids 40+ MB
  ARGB_8888 allocations and the GC churn that follows).
- Switch ML Kit clients to var + lazy-on-first-use so close() no
  longer triggers init for clients we never invoked.
- Wrap the composable's suggestAltText call in try/finally so a
  cancellation mid-inference resets the spinner state.
2026-04-27 13:54:18 +00:00
Claude 952e0a2192 feat(ai): prefer genai image description, fall back to image labeling
Adds com.google.mlkit:genai-image-description as the primary alt-text
source — Gemini Nano via AICore produces full descriptive sentences on
supported devices. When checkFeatureStatus reports anything other than
AVAILABLE (or AICore is missing), the service falls back to the legacy
play-services-mlkit-image-labeling keyword join. Both paths sit behind
the same MLKitImageLabelService.suggestAltText API; the F-Droid stub is
unchanged.
2026-04-27 13:32:57 +00:00
Claude d8cb5c66ec feat(ai): suggest alt-text via on-device image labeling
Wire ML Kit image labeling into the media-attach dialog so the alt-text
field is prefilled with a confidence-filtered, comma-separated label
list when the user picks an image and the field is still empty. A
spinner shows during labeling and a dismissible "AI-suggested, edit me"
chip lets the user revert. Play flavor uses
play-services-mlkit-image-labeling; F-Droid ships a no-op stub.
2026-04-27 13:12:00 +00:00
Claude fd03122206 docs(nestsClient/specs): close hosting-side ambiguities
Define the host-side flow that was previously implicit. A new-room
implementer can now go from "I want to start broadcasting" to first
audio frame without reading our source.

README — new "Hosting a new room" section:
- 8-step ASCII walkthrough symmetrical to "Joining sequence".
- Covers service/endpoint selection, kind:30312 compose + publish, JWT
  mint with publish:true, WT/Setup, Announce, presence, Opus stream.
- Lists ongoing host duties (add speaker, kick, edit, close, recording).

EGG-01 — two new behavior rules:
- Rule 12 "Publish-before-mint ordering": peers MUST publish the
  kind:30312 to relays BEFORE requesting a JWT for it, since the auth
  sidecar reads the most-recent event by (pubkey, kind, d) to validate
  existence / status. 410 unknown_room → retry 1s/2s/4s. Closes the
  silent footgun where a host could mint a token before their event
  has propagated.
- Rule 13 "Service/endpoint selection (host-side guidance)": pre-fill
  from kind:10112 first entry, fall back to client default with user
  override, both MUST be https://.

EGG-07 — new "Audio publish authorisation" section:
- Spells out who gets a publish:true JWT: host (by authorship,
  implicitly — no need for ["p", _, _, "speaker"] on the host's own
  p-tag), explicit "speaker" role, or "admin" role. All others 403
  publish_forbidden per EGG-02.
- Relay does NOT re-read kind:30312; demoting a speaker mid-session
  does NOT terminate their stream — host MUST kick (kind:4312) to
  end an active broadcast.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
2026-04-27 13:05:49 +00:00
Claude bd12d5ef72 docs(nestsClient/specs): close interop gaps surfaced in spec review
Edits across all 13 EGGs to remove implementer guesswork. After this an
implementer can build a listener and speaker without reading our source.

README:
- Conventions section: hex casing rule (lowercase, 64 chars, no 0x),
  NIP-01 foundations, `a`-tag form, created_at tie-break, JSON / time.
- Joining sequence: numbered end-to-end walkthrough from `kind:30312`
  to first audio frame, referencing each EGG.

EGG-01 (room event):
- `relays` tag is one tag with multiple values (not multi-tag).
- `service` URL trailing-slash normalization.
- `private` status: gate is implementation-defined, not "render as open".
- `d`-tag charset locked to [A-Za-z0-9._-] so it interpolates safely
  into the moq-auth namespace.
- Relay-discovery rule: publish to `relays` tag ∪ NIP-65 outbox.
- Tie-break on identical created_at via smallest event id.

EGG-02 (auth):
- JWT signing pinned: ES256 over P-256, JWKS at /.well-known/jwks.json,
  5-minute relay cache.
- NIP-98 tags pinned: u / method / payload, base64 RFC 4648 standard
  (not base64url).
- Error taxonomy: full HTTP status + `error` slug table for /auth, plus
  WebTransport CONNECT 200/401/403/404 table.

EGG-03 (audio):
- Pin moq-lite Lite-03 to kixelated/moq-rs `rs/moq-lite/src/lite/`.
- One Opus packet per moq Frame (no container, no timestamp).
- Pubkey hex casing reaffirmed at the suffix / broadcast slots.
- AnnouncePlease prefix="" for speaker discovery.
- Mute = stop publishing (not silence frames, not Announce Ended).
- Mid-stream join: discard pre-skip per RFC 7845.

EGG-04 (presence):
- Heartbeat jitter ±5 s required (anti-thundering-herd).
- "0"/"1" are strings, not booleans.
- Single-room rule via replaceable-event semantics.

EGG-05 (chat): 8 KB suggested, 64 KB hard cap, 3 msg/s render rate.
EGG-06 (reactions): 30s window measured against created_at, drop-on-arrival
                    if already stale.
EGG-07 (moderation): replay protection — dedupe kicks by id within 120 s.
EGG-08 (scheduling): planned rooms MUST 403 at the auth sidecar.
EGG-10 (theming): bg image caps (1 MB / 4096 px soft, 8 MB / 8192 px hard).

Deferred (per review): EGG-00 (Conventions as a standalone spec), EGG-13
(capability advertisement), EGG-14 (discovery), test vectors corpus.
The "Conventions" section in README covers EGG-00's most urgent content
inline.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
2026-04-27 12:31:19 +00:00
Vitor Pamplona 3e3562ee42 Merge pull request #2597 from davotoula/fix-chat-cursor-jump
Fix chat cursor jump
2026-04-27 08:07:50 -04:00
Vitor Pamplona 7ed7684ff0 Merge pull request #2596 from nrobi144/fix/libicu74-dependency
fix(packaging): relax libicu dependency in .deb for cross-distro compat
2026-04-27 07:59:17 -04:00
davotoula c78c133675 test(compose): instrumented coverage for MentionPreservingInputTransformation
11 cases driving the predicate matrix against a real TextFieldState on
device:

  - mention-free text passes through
  - pure delete fully covering a mention is allowed
  - partial overlap (at start, at end, inside) collapses atomically
  - scope-exact replace with non-empty text collapses (SwiftKey case)
  - scope-broader replace passes through
  - append after mention preserves it
  - trailing space and trailing newline are consumed during atomic collapse
  - multiple mentions: only the touched one collapses
  - cheap-gate path (mention-free original) is verified

All 11 pass on Pixel 9a; gives the predicate a regression net so future
predicate-tuning doesn't reintroduce the @Vitor Pamplona bug.

Run via:
  ./gradlew :amethyst:connectedPlayDebugAndroidTest \
    -Pandroid.testInstrumentationRunnerArguments.class=\
com.vitorpamplona.amethyst.MentionPreservingInputTransformationTest

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 10:53:49 +02:00
davotoula 338080115f fix(compose): also collapse on scope-exact replace; user-reported @Vitor Pamplona regression
fix(compose): tighten @OptIn scope from @file to the object
fix(compose): allow full-cover changes through; collapse only on partial overlap
refactor(compose): hoist MENTION_REGEX, fast-path mention-free text, drop redundant scaffolding
fix(compose): also collapse mention atomically on full-range non-empty replaces
fix(compose): atomically delete the whole mention on partial-overlap edits
fix(compose): opt-in ExperimentalFoundationApi in MentionPreservingInputTransformation
2026-04-27 10:53:49 +02:00
nrobi144 450c740c62 fix(packaging): add trap cleanup and xz compression to deb rewriter
Address review findings:
- Add trap for temp dir cleanup on error
- Use -Zxz for max distro compatibility (older dpkg lacks zstd)
- Use --root-owner-group for correct file ownership

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 10:39:24 +03:00
Claude 5c40e27fde docs(nestsClient/specs): EGG-00..EGG-12 — Extensible Gossip Guidelines
Wire-protocol specs for nostrnests-style audio rooms, in the
style of Nostr NIPs and Blossom BUDs. Each spec documents one
self-contained capability that a client or relay can implement;
two compliant peers implementing the same set of EGGs round-trip
without further coordination.

Layout:

  README.md            cover, status table, conformance levels
  EGG-01.md  Room event (kind:30312)              required
  EGG-02.md  Auth + WebTransport handshake        required
  EGG-03.md  Audio plane (moq-lite)               required
  EGG-04.md  Presence (kind:10312)                required
  EGG-05.md  In-room chat (kind:1311)             optional
  EGG-06.md  Reactions (kind:7)                   optional
  EGG-07.md  Roles & moderation (kind:4312)       optional
  EGG-08.md  Scheduling (status=planned)          optional
  EGG-09.md  User server list (kind:10112)        optional
  EGG-10.md  Theming (c/f/bg tags)                decorative
  EGG-11.md  Recording                            decorative
  EGG-12.md  Catalog track (catalog.json)         optional

Conformance levels (Listener / Speaker / Host) defined in the
README so a deployment can declare "we implement EGG-01..EGG-04"
and other peers know exactly what to expect.

Each spec follows the same shape (Summary / Wire format /
Behavior numbered MUST/SHOULD/MAY rules / Example /
Compatibility) and fits on a single printed page. Wire formats
are documented exactly as nostrnests + amethyst implement them
on this branch — no hypothetical capabilities, no "future" tags
without an EGG number.
2026-04-27 03:54:45 +00:00
Claude 538525f003 feat(audio-rooms): home live-bubble surfaces follows broadcasting in a room
Companion path to the chat-driven inclusion landed in the
previous commit. The home filter now ALSO pulls a kind-30312
audio room into the live-bubble row when a follow is actively
broadcasting in it (kind-10312 presence with `publishing=1`).

Wire-up:

  * LocalCache gains a dedicated consume(MeetingRoomPresenceEvent)
    that, in addition to the addressable storage, attaches the
    version note to the room's LiveActivitiesChannel keyed by
    the kind-30312 address from the `["a", ...]` tag. The same
    fan-out kind-1311 chat already gets, applied to presence.
    Without this, channel.notes never sees presence events and
    the bubble can't pick them up via the chat-channel pump.

  * HomeLiveFilter.acceptableChatEvent extends to recognize
    MeetingRoomPresenceEvent. We only accept publishing=true
    presences from a follow targeting an OPEN/PRIVATE room —
    hand-raise / mute / pure-listener heartbeats are noise that
    would flood the bubble with everyone in the room every 30 s.
    Status check shares the new isMeetingSpaceLive() helper with
    the chat-driven path.

  * HomeLiveFilter.updateListWith resolves the room channel for
    incoming MeetingRoomPresenceEvents via the same
    `interactiveRoom().address` pointer.

End user effect: a follow opening their mic in an audio room is
now indistinguishable from them chatting in it — both pull the
room into the live-bubble row, both surface the room name + tap
straight into AudioRoomActivity. Bubble disappears within 15 min
of the last qualifying event (chat or presence) per the existing
window.
2026-04-27 03:37:19 +00:00
Claude 44533b2187 feat(audio-rooms): surface in home live-bubble row
When a follow chats in a kind-30312 audio room, the room now
appears in the live-bubble row at the top of home — same place
streaming kind-30311 + ephemeral chats already show up.

The plumbing already exists below the surface:
LocalCache.consume(LiveActivitiesChatMessageEvent) calls
getOrCreateLiveChannel(activityAddress) regardless of whether
the address is a kind-30311 or kind-30312, so liveChatChannels
already grows entries for audio rooms whenever a follow chats
in one. The home filter just rejected them at the
acceptableChatEvent guard because it required `info.status() ==
LIVE` — and `info` is hard-typed to LiveActivitiesEvent so it's
null for audio rooms.

Three changes:

  * HomeLiveFilter.acceptableChatEvent — branch on the chat's
    activityAddress.kind. For kind-30311 keep the existing
    info.status() == LIVE path; for kind-30312 read the
    addressable's MeetingSpaceEvent and accept OPEN/PRIVATE.
    "Closed" rooms drop out of the bubble even if a follow is
    chatting in the (now-archived) chat.

  * RenderLiveActivityBubble — when channel.address.kind ==
    30312, pull room title from the addressable so the bubble
    label reads "Lounge" instead of "naddr1abc…", and tap-launch
    AudioRoomActivity directly (one-tap into the room) instead
    of routing to ChannelView. Falls back to the channel route
    if the address is malformed.

  * LiveStatusIndicator.checkChannelIsOnline — the red live-dot
    surfaces for kind-30312 channels whenever their addressable
    is OPEN/PRIVATE, mirroring what status==LIVE means for
    streaming.

Audio rooms with no chat yet still don't surface (would require
populating channel.info, which would mean widening the channel
model — deferred to a later cycle per the architectural
discussion). The chat-driven case covers nostrnests' typical UX:
a host opens a room, audience members start chatting, the bubble
pulls in their followers.
2026-04-27 03:28:01 +00:00
Claude e3bf46377b fix(audio-rooms): unwrap hiddenUsers State for the feed key (audit walk #9)
Pre-existing bug surfaced during the user-walkthrough audit:
WatchAccountForAudioRoomsScreen used `val hiddenUsers =
hiddenUsers.flow.collectAsStateWithLifecycle()` (no `by`), so the
LaunchedEffect captured a State<T> object as a key rather than
the unwrapped value. State equality is reference-based and the
remembered State instance is stable across recompositions, so the
effect never re-fired when the user muted someone — the rooms
feed showed stale results until a manual refresh.

Switching to `val hiddenUsers by ...` unwraps the value so
LaunchedEffect re-keys when the hidden-users set changes.
2026-04-27 02:53:53 +00:00
Claude e041c77832 fix(audio-rooms): UX rough edges from the screen walk (audit walk #5-8)
Four user-visible quirks the walkthrough surfaced:

  5. EditAudioRoomSheet's "Close room" button was one tap with no
     confirm. A misclick destroyed the room. Added an AlertDialog
     gate ("All attendees will be disconnected. The room will
     show as CLOSED in the feed.") with a destructive primary
     action and a Cancel.

  6. Silent ActivityNotFoundException in ParticipantHostActionsSheet
     (View Profile) and MeetingSpace.kt (Listen to Recording).
     Both were `runCatching { startActivity(...) }` with no toast
     on failure — user taps, nothing happens, no feedback. Now
     they toast "No app installed to open this link." through
     the standard toastManager.

  7. ScheduleStartPicker dismiss didn't revert in-dialog state.
     User picks Dec 13, taps Cancel, reopens the dialog → still
     pre-selected to Dec 13 (the cancelled choice) instead of the
     committed value. Added `resetPickersToCommitted()` that
     rewinds both picker states to the committed unixSeconds on
     every dismiss / cancel path.

  8. CreateAudioRoomViewModel.publishAndBuildLaunchInfo accepted
     past start times. Added a `scheduledStartUnix < now` guard so
     the host gets "Pick a future start time." instead of publishing
     a kind-30312 with `status=planned` + a backdated `starts` tag.
2026-04-27 02:52:55 +00:00
Claude 9af95103e8 fix(audio-rooms): chat send / scroll / display name (audit walk #3, #4)
Three rough edges in the in-room chat panel:

  3. Send swallowed broadcast failures and cleared the draft
     unconditionally. An offline user typed, tapped Send, watched
     the text vanish, and never saw a toast — the message landed
     nowhere. Now the draft clears ONLY on success; failure toasts
     `audio_room_chat_send_failed_title` with the exception
     message and keeps the text in the field for retry. Composer
     also disables itself for the brief in-flight window so a
     double-tap can't fire two sends.

  4. Auto-scroll forced to bottom on every new message, even
     while the user was reading older messages. Switched to the
     standard "only scroll if pinned near the bottom" pattern via
     a derivedStateOf gate over `listState.firstVisibleItemIndex`.

  + Replaced the v1 placeholder (truncated 8-hex of the pubkey)
    with the canonical Amethyst chat author-name pattern:
    LoadUser kicks the metadata fetch, UsernameDisplay observes
    the kind-0 flow, and the result falls back to a truncated
    npub while the metadata is in flight or missing. Same code
    path RenderChatClip / RenderChatRaid use.
2026-04-27 02:49:36 +00:00
Claude e79f78fc28 fix(audio-rooms): keep state alive across Reconnecting blips (audit walk #1, #2)
Two reliability bugs the user-walkthrough audit caught:

  1. Foreground service flickered on every transport blip. The
     LaunchedEffect keyed on `isConnected = ui.connection is
     Connected`, so a Reconnecting state stopped the service
     mid-blip and restarted it on recovery. Beyond the audible
     dropout from losing the wake-lock, Android 14+ doesn't
     reliably let us re-promote to FOREGROUND_SERVICE_TYPE_MICROPHONE
     after losing it — risking a permanent broadcast-side regression
     after a single relay hiccup. Treat Reconnecting as "still
     live" for the service decision.

  2. TalkRow disappeared mid-broadcast on transient listener
     disconnect. The speaker session is INDEPENDENT of the
     listener — the user could be broadcasting cleanly while the
     listener is Reconnecting, but our `if (ui.connection !is
     Connected) return` early-exited the entire row, so the user
     could neither mute their mic nor stop their broadcast until
     the listener recovered. Switch the gate to "user can act on
     broadcast state" — Connected listener OR an in-flight
     broadcast handle (Connecting/Broadcasting).

Same logic applied to the PIP-entry gate so onUserLeaveHint
during a transient blip doesn't lock the user out of PIP.
2026-04-27 02:47:02 +00:00
Claude 54584c213f perf(audio-rooms): hoist ParticipantsGrid per-cell allocations (audit #9)
Each cell of the participant grid was allocating fresh Modifier
chains and lambdas on every recompose. With a 50-speaker room
and the grid recomposing on every connectingSpeakers /
speakingNow / reactions flip, the per-frame allocation count
adds up.

Hoist the constants:
  * gridModifier (fillMaxWidth + height + padding)
  * cellWidthModifier (avatarSize + 16.dp)
  * absentAlphaModifier (alpha 0.5f)
  * speakingBorderModifier (border + CircleShape)
  * spinnerModifier (size avatarSize - 8.dp)

Replace the `.let { ... }.let { ... }` Modifier chain on the
avatar with a `when` over the four (isSpeaking × absent) cases —
each case picks a pre-built Modifier rather than synthesising a
fresh chain.

Cache the per-pubkey long-click adapter via remember(pubkey,
onLongPressParticipant) so the `{ hex -> cb(hex) }` wrapper
isn't re-allocated on every recompose.
2026-04-27 02:30:13 +00:00
Claude 43c5313674 chore(audio-rooms): align catalog/announces error timing + comment (audit #5-7)
* announces() now throws UnsupportedOperationException at CALL
    time (not the first collect) on the IETF default — matches
    subscribeCatalog's timing so both can be guarded with one
    `runCatching { listener.announces() }` per session rather than
    a runCatching around every collect site (audit #6).

  * KDoc on announces() documents the deliberate hot-vs-cold
    asymmetry with subscribeSpeaker/subscribeCatalog: announce
    data is room-state with a single VM consumer (cold flow is
    sufficient), audio is per-frame playout with multi-collect
    resilience needs (hot SharedFlow with DROP_OLDEST). The
    different shapes are intentional, not accidental (audit #5).

  * MoqLiteNestsListener.wrapSubscription's "IETF SubscribeHandle
    path conventionally surfaces" comment was scoped to the audio
    track when first written; now the same body serves both audio
    and catalog. Re-frame so the comment applies to either track
    (audit #7).

Test: NestsListenerCatalogTest's announces-throws case now
asserts the call itself throws, not the collect.
2026-04-27 02:28:24 +00:00