feat(audio-rooms): "Listen to recording" CTA for closed rooms
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<MeetingSpaceEvent>.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.
This commit is contained in:
@@ -101,6 +101,7 @@ fun RenderMeetingSpaceEventInner(
|
|||||||
val summary = remember(eventUpdates) { noteEvent.summary() }
|
val summary = remember(eventUpdates) { noteEvent.summary() }
|
||||||
val status = remember(eventUpdates) { noteEvent.status() }
|
val status = remember(eventUpdates) { noteEvent.status() }
|
||||||
val starts = remember(eventUpdates) { noteEvent.starts() }
|
val starts = remember(eventUpdates) { noteEvent.starts() }
|
||||||
|
val recording = remember(eventUpdates) { noteEvent.recording() }
|
||||||
val participants = remember(eventUpdates) { noteEvent.participants() }
|
val participants = remember(eventUpdates) { noteEvent.participants() }
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@@ -160,17 +161,20 @@ fun RenderMeetingSpaceEventInner(
|
|||||||
|
|
||||||
RenderParticipants(participants, accountViewModel, nav)
|
RenderParticipants(participants, accountViewModel, nav)
|
||||||
|
|
||||||
// In-feed Join button — closes the deep-link loop. A
|
// In-feed CTA. Live / scheduled rooms get a Join button; closed
|
||||||
// `nostr:naddr1...` for a kind-30312 lands here through
|
// rooms with a recording get a "Listen to recording" button so
|
||||||
// MainActivity's URI handler → Route.Note(addressTag) → this
|
// audience members who missed the live session can listen back.
|
||||||
// renderer; without a button the user has no path into the
|
// Closed rooms without a recording stay button-less — there's
|
||||||
// audio room. JoinAudioRoomButton hides itself when the
|
// nothing to enter.
|
||||||
// event lacks service / endpoint / d-tag.
|
Row(
|
||||||
if (status != MeetingSpaceStatusTag.STATUS.CLOSED) {
|
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||||
Row(
|
horizontalArrangement = Arrangement.End,
|
||||||
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(
|
com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room.JoinAudioRoomButton(
|
||||||
event = noteEvent,
|
event = noteEvent,
|
||||||
accountViewModel = accountViewModel,
|
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
|
@Composable
|
||||||
fun RenderMeetingRoomEvent(
|
fun RenderMeetingRoomEvent(
|
||||||
baseNote: Note,
|
baseNote: Note,
|
||||||
|
|||||||
@@ -503,6 +503,7 @@
|
|||||||
<string name="audio_room_connecting_handshake">Negotiating audio</string>
|
<string name="audio_room_connecting_handshake">Negotiating audio</string>
|
||||||
<string name="audio_room_connected">Audio connected</string>
|
<string name="audio_room_connected">Audio connected</string>
|
||||||
<string name="audio_room_reconnecting">Reconnecting…</string>
|
<string name="audio_room_reconnecting">Reconnecting…</string>
|
||||||
|
<string name="audio_room_listen_to_recording">Listen to recording</string>
|
||||||
<string name="audio_room_audio_failed">Audio failed: %1$s</string>
|
<string name="audio_room_audio_failed">Audio failed: %1$s</string>
|
||||||
<string name="audio_room_audio_unavailable">Audio is not available for this room</string>
|
<string name="audio_room_audio_unavailable">Audio is not available for this room</string>
|
||||||
<string name="audio_room_talk">Talk</string>
|
<string name="audio_room_talk">Talk</string>
|
||||||
|
|||||||
+9
@@ -93,6 +93,15 @@ class MeetingSpaceEvent(
|
|||||||
*/
|
*/
|
||||||
fun font() = tags.firstNotNullOfOrNull(com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.FontTag::parse)
|
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
|
* Scheduled start time as unix seconds, only meaningful when
|
||||||
* [status] is [StatusTag.STATUS.PLANNED]. Returns null on
|
* [status] is [StatusTag.STATUS.PLANNED]. Returns null on
|
||||||
|
|||||||
+6
@@ -61,6 +61,12 @@ fun TagArrayBuilder<MeetingSpaceEvent>.font(
|
|||||||
.assemble(family, url),
|
.assemble(family, url),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun TagArrayBuilder<MeetingSpaceEvent>.recording(url: String) =
|
||||||
|
addUnique(
|
||||||
|
com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.RecordingTag
|
||||||
|
.assemble(url),
|
||||||
|
)
|
||||||
|
|
||||||
fun TagArrayBuilder<MeetingSpaceEvent>.relays(urls: List<NormalizedRelayUrl>) = addUnique(RelayListTag.assemble(urls))
|
fun TagArrayBuilder<MeetingSpaceEvent>.relays(urls: List<NormalizedRelayUrl>) = addUnique(RelayListTag.assemble(urls))
|
||||||
|
|
||||||
fun TagArrayBuilder<MeetingSpaceEvent>.participant(
|
fun TagArrayBuilder<MeetingSpaceEvent>.participant(
|
||||||
|
|||||||
Reference in New Issue
Block a user