Commit Graph

12334 Commits

Author SHA1 Message Date
Claude befa13d797 Merge remote-tracking branch 'origin/main' into claude/audio-rooms-android-ui-PIpFN 2026-04-27 19:00:48 +00:00
Claude 3e19651d22 feat(nests): show like + zap row on Nest / MeetingRoom cards
Discovery's RenderLiveActivityThumb cards already render a creator
+ name + like + zap row at the bottom — but only when the channel
holds a kind:30311 LiveActivitiesEvent. Nests (kind:30312) and
Meeting Rooms (kind:30313) populate channels with the same address
shape but `LiveActivitiesChannel.info` is null for them, so the
reactions row was hidden.

ShortLiveActivityChannelHeader:
  - Add an `addressableNote: Note?` parameter to the inner overload.
    The outer (LiveActivitiesChannel) overload fills it from
    `LocalCache.getAddressableNoteIfExists(channel.address)`, which
    works for any kind that registers an addressable note.
  - Move the like/zap row OUT of the `liveActivitiesEvent?.let { }`
    gate so it renders for any addressable note — kind:30311,
    30312, 30313 all share it now.
  - Keep the LIVE / OFFLINE flag inside the LiveActivitiesEvent gate
    (it pings the streaming URL — stream-specific). Extracted into
    a small `LiveChannelLiveFlag` helper so the reaction row can
    render independently.

MeetingRoomsFeedLoaded:
  - Swap from RenderMeetingRoomEvent to RenderLiveActivityThumb so
    the Meeting Rooms list visually matches the Nests list and
    Discovery — same cover-image card with status flag, participant
    gallery, and bottom header (creator + name + reactions).

Other call sites (LiveActivityTopBar, LiveActivitiesChannelHeader)
use the outer `baseChannel` overload and pick up the new behavior
automatically.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
2026-04-27 18:51:33 +00:00
Vitor Pamplona 09f653950e Merge pull request #2604 from vitorpamplona/claude/fix-nav-back-arrow-99NGn
Fix back stack management in bottom navigation
2026-04-27 14:49:03 -04:00
Claude 0e4379c5bf fix(nav): clear back stack on bottom-nav taps so no back arrow shows
popUpTo(route) { inclusive = true } only pops if `route` is already in
the stack. From Home, tapping any other bottom-nav tab left Home in the
back stack, so canPop() returned true and the back arrow appeared on a
root tab.

Pop up to the graph's start destination instead (also inclusive). This
clears Home — and any drawer/deep-push entries above it — before
navigating to the new bottom-nav root, so each bottom-nav tap leaves
exactly one entry in the stack.

https://claude.ai/code/session_01PrirRcL7g8iX7vTqqLTkBS
2026-04-27 18:39:23 +00:00
Claude 4782b7b6ac feat(meetingrooms): split kind:30313 into its own drawer screen
Nests (kind:30312) and Meetings (kind:30313) were sharing one drawer
entry / one feed; the only thing they have in common is being NIP-53
events. They have different lifecycles (audio rooms vs scheduled
meetings) and different status enums (open/private/closed/planned vs
live/planned/ended). Mixing them under the Nests label was confusing.

Split:

- MeetingRoomsFeedFilter (new) — narrows to MeetingRoomEvent (30313),
  with title-must-be-non-blank gate (mirrors the EGG-01-style minimum-
  fields rule the Nests feed applies). Sort by status (LIVE > PLANNED
  > ENDED), then participants among follows, then `starts` / created.
- NestsFeedFilter (existing) — narrowed to MeetingSpaceEvent (30312)
  only. Drops the dual-kind branches in `innerApplyFilter` and the
  parallel sub-room enum in `convertStatusToOrder`. Same EGG-01 gate
  (room/status/service/endpoint).

UI:

- New MeetingRoomsScreen / MeetingRoomsFeedLoaded / MeetingRoomsTopBar.
  Read-only — the app doesn't compose kind-30313 events locally; tap
  routes a card to the standard thread view via `routeFor`.
- Reuses the existing in-feed renderer
  (`note.types.RenderMeetingRoomEvent`) for visual consistency
  with the Note thread surface.
- Mounts NestsFilterAssemblerSubscription so the wire side stays a
  single REQ regardless of which screen the user opens first
  (`makeLiveActivitiesFilter` already covers 30311/30312/30313/1311
  in one filter).

Wiring:

- Route.MeetingRooms → MeetingRoomsScreen in AppNavigation.
- NavBarItem.MEETING_ROOMS in the catalog (Groups icon, label
  "Meeting Rooms"). Same isDebug gate as NESTS while the surface is
  still in development.
- meetingRoomsFeed in AccountFeedContentStates (mirroring nestsFeed
  for updateFeedWith / deleteFromFeed propagation).
- ScrollStateKeys.MEETING_ROOMS_SCREEN.
- New string `meeting_rooms` ("Meeting Rooms").

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
2026-04-27 18:36:10 +00:00
Claude 14953706b6 refactor(nests): split NestActivityBody into themed effect helpers
The 280-line `NestActivityBody` orchestrated VM creation, four event
collectors, two eviction tickers, kick handling, two PIP bridges, the
foreground-service lifecycle, the kind-10312 heartbeat / debounce /
final-leave publisher, AND the screen routing. Each concern was clear
on its own but lost in the wall of LaunchedEffects.

Split into three sibling files, each named for its concern. The body
shrinks to ~70 lines of named composable wiring calls — the room's
lifecycle is now legible end-to-end at a glance.

NestRoomLifecycle.kt — VM + activity bridges:
  rememberNestViewModel(room, signer)
  AutoConnectAndTrackSpeakers(viewModel, onStageKeys)
  LeaveOnKick(viewModel, onLeave)
  PipBridge(ui, pipMuteSignal, viewModel, onMuteState, onConnectedChange)
  NestForegroundServiceLifecycle(ui)

NestRoomEventCollectors.kt — read-side LocalCache → VM:
  NestRoomEventCollectors(viewModel, event, roomATag, localPubkey)
  internally:
    PresenceCollector + PresenceEvictionTicker
    ChatCollector
    ReactionsCollector + ReactionsEvictionTicker
    AdminCommandsCollector  (with the host/moderator authority gate)

NestRoomPresencePublisher.kt — write-side kind-10312:
  NestPresencePublisher(account, event, ui, handRaised)
  internally: heartbeat (30 s) + mute-debounce (500 ms) + onDispose
  final leave on a non-cancellable scope so it survives mid-network
  scope cancellation.
  publishPresence helper relocates here too.

NestActivityContent.kt — now 170 lines:
  - NestActivityContent (entry, parses address)
  - NestActivityBody (orchestration only, ~70 lines)
  - publishPresence helper moved out
  - dropped the half-dozen now-unused imports

Behavior identical: every LaunchedEffect / DisposableEffect ran
before the split runs after, with the same keys, same dispatchers,
same scopes. No new coroutines, no removed.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
2026-04-27 18:25:03 +00:00
Claude f5984e00c5 refactor(nests): collapse 4 per-room subs into one NestRoomFilterAssembler
The room screen used to fan out four parallel relay subscriptions
through separate ComposeSubscriptionManagers — RoomChat, RoomPresence,
RoomReactions, RoomAdminCommands — each opening its own REQ per outbox
relay. They all keyed on the same dimensions (roomATag, account) and
ran in the same place (NestActivityContent), so the multi-sub
fan-out was wire waste.

Replace with a single NestRoomFilterAssembler that issues two Filters
inside one REQ per relay:

  - {kinds: [1311, 10312, 7], "#a": [roomATag]}
      → chat, presence, reactions on the shared a-tag gate
  - {kinds: [4312], "#a": [roomATag], "#p": [localPubkey]}
      → admin commands; #p restriction stays on a separate Filter so
        it doesn't over-restrict the chat / presence / reaction kinds

Net wire change: 4 REQs per relay → 1 REQ per relay with 2 Filters,
same event coverage. Same kinds, same #a/#p semantics, same outbox
relay set.

Files removed:
  RoomChatFilterAssembler.kt
  RoomPresenceFilterAssembler.kt
  RoomPresenceFilterAssemblerSubscription.kt
  RoomReactionsFilterAssembler.kt
  RoomAdminCommandsFilterAssembler.kt

Files updated:
  NestActivityContent.kt — one NestRoomFilterAssemblerSubscription
    call replaces four. The per-kind LocalCache.observeEvents
    LaunchedEffects stay; only the wire side got collapsed.
  RelaySubscriptionsCoordinator.kt — register `nestRoom` instead of
    the four old per-room assemblers.

NestsFilterAssembler (the rooms-list level subscription) is unchanged
— different keying (per-user, per-follow-list), different lifecycle.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
2026-04-27 18:18:48 +00:00
Claude 4796dceb9e fix(nests): respect status / nav bar insets in NestFullScreen
The room content was sliding under the status bar at the top and the
gesture / nav bar at the bottom because the outer Column only had
fillMaxSize + 16dp content padding. With Amethyst running edge-to-edge
the title cropped behind the status bar and the chat composer / Leave
button sat under the system nav bar.

Wrap the content Column in `windowInsetsPadding(WindowInsets.safeDrawing)`
INSIDE NestThemedScope but BEFORE the existing 16dp padding, so:
  - The themed background color + optional `bg` image still paint
    edge-to-edge (NestThemedScope's outer Box stays full-bleed).
  - The room body — title, participants, chat, talk row, Leave —
    is inset within the safe-drawing area.

PIP screen unchanged; the system manages PIP layout itself.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
2026-04-27 18:07:07 +00:00
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