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(