From 3fb9f02b6d41d690cac408883a06917244fdf018 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 01:29:16 +0000 Subject: [PATCH] feat(audio-rooms): "Listen to recording" CTA for closed rooms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire MeetingSpaceEvent's existing RecordingTag into the note-view renderer. Closed rooms (status=CLOSED) that ship a `["recording", url]` tag get an OutlinedButton that hands the URL to the system media player via ACTION_VIEW — most users have a podcast / audio app registered for HTTPS audio URLs. Live and scheduled rooms keep the Join button. Closed rooms without a recording render no CTA — there's nothing to enter. Adds: * MeetingSpaceEvent.recording() accessor * TagArrayBuilder.recording(url) DSL builder * ListenToRecordingButton composable in the note renderer Note: nostrnests' moq-lite refactor dropped the LiveKit-era recording HTTP surface, but the kind-30312 `recording` tag is still spec-allowed and individual hosts can populate it via out-of-band capture. This commit makes Amethyst the receiving end for any host who does. --- .../amethyst/ui/note/types/MeetingSpace.kt | 49 ++++++++++++++----- amethyst/src/main/res/values/strings.xml | 1 + .../meetingSpaces/MeetingSpaceEvent.kt | 9 ++++ .../meetingSpaces/TagArrayBuilderExt.kt | 6 +++ 4 files changed, 54 insertions(+), 11 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt index eae46e8aa..e8071ca2b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt @@ -101,6 +101,7 @@ fun RenderMeetingSpaceEventInner( val summary = remember(eventUpdates) { noteEvent.summary() } val status = remember(eventUpdates) { noteEvent.status() } val starts = remember(eventUpdates) { noteEvent.starts() } + val recording = remember(eventUpdates) { noteEvent.recording() } val participants = remember(eventUpdates) { noteEvent.participants() } Row( @@ -160,17 +161,20 @@ fun RenderMeetingSpaceEventInner( RenderParticipants(participants, accountViewModel, nav) - // In-feed Join button — closes the deep-link loop. A - // `nostr:naddr1...` for a kind-30312 lands here through - // MainActivity's URI handler → Route.Note(addressTag) → this - // renderer; without a button the user has no path into the - // audio room. JoinAudioRoomButton hides itself when the - // event lacks service / endpoint / d-tag. - if (status != MeetingSpaceStatusTag.STATUS.CLOSED) { - Row( - modifier = Modifier.fillMaxWidth().padding(top = 8.dp), - horizontalArrangement = Arrangement.End, - ) { + // In-feed CTA. Live / scheduled rooms get a Join button; closed + // rooms with a recording get a "Listen to recording" button so + // audience members who missed the live session can listen back. + // Closed rooms without a recording stay button-less — there's + // nothing to enter. + Row( + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + horizontalArrangement = Arrangement.End, + ) { + if (status == MeetingSpaceStatusTag.STATUS.CLOSED) { + recording?.let { + ListenToRecordingButton(url = it) + } + } else { com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room.JoinAudioRoomButton( event = noteEvent, accountViewModel = accountViewModel, @@ -179,6 +183,29 @@ fun RenderMeetingSpaceEventInner( } } +/** + * Hand off a kind-30312 `recording` URL to the system media + * player via ACTION_VIEW. Most users have a podcast / audio app + * registered for `https://...mp3`-style URLs; the system picker + * lets them choose. Stays out of the in-app audio path on + * purpose — Amethyst doesn't ship its own audio player surface. + */ +@Composable +private fun ListenToRecordingButton(url: String) { + val context = androidx.compose.ui.platform.LocalContext.current + androidx.compose.material3.OutlinedButton(onClick = { + runCatching { + context.startActivity( + android.content + .Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(url)) + .addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK), + ) + } + }) { + Text(stringRes(R.string.audio_room_listen_to_recording)) + } +} + @Composable fun RenderMeetingRoomEvent( baseNote: Note, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 671524153..b1446d939 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -503,6 +503,7 @@ Negotiating audio Audio connected Reconnecting… + Listen to recording Audio failed: %1$s Audio is not available for this room Talk diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/meetingSpaces/MeetingSpaceEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/meetingSpaces/MeetingSpaceEvent.kt index ca4072028..e1ea88944 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/meetingSpaces/MeetingSpaceEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/meetingSpaces/MeetingSpaceEvent.kt @@ -93,6 +93,15 @@ class MeetingSpaceEvent( */ fun font() = tags.firstNotNullOfOrNull(com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.FontTag::parse) + /** + * Optional URL to a post-room recording. Hosts that record a + * room re-publish the kind-30312 with `status=closed` and + * `["recording", url]` so audience members who missed the live + * session can listen back. Returns null when no recording is + * available. + */ + fun recording() = tags.firstNotNullOfOrNull(com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.RecordingTag::parse) + /** * Scheduled start time as unix seconds, only meaningful when * [status] is [StatusTag.STATUS.PLANNED]. Returns null on diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/meetingSpaces/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/meetingSpaces/TagArrayBuilderExt.kt index 1fb762a54..d676c4f34 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/meetingSpaces/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/meetingSpaces/TagArrayBuilderExt.kt @@ -61,6 +61,12 @@ fun TagArrayBuilder.font( .assemble(family, url), ) +fun TagArrayBuilder.recording(url: String) = + addUnique( + com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.RecordingTag + .assemble(url), + ) + fun TagArrayBuilder.relays(urls: List) = addUnique(RelayListTag.assemble(urls)) fun TagArrayBuilder.participant(