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 eae46e8aa7..e8071ca2b7 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 6715241533..b1446d9395 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 ca40720288..e1ea889446 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 1fb762a541..d676c4f34b 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(