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
This commit is contained in:
+91
-1
@@ -33,17 +33,21 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.AssistChipDefaults
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.FilledTonalIconToggleButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -64,7 +68,9 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNAddr
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.StatusTag
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Full-screen layout for [AudioRoomActivity]. Renders title + summary,
|
||||
@@ -98,7 +104,9 @@ internal fun AudioRoomFullScreen(
|
||||
) {
|
||||
var showEditSheet by rememberSaveable { mutableStateOf(false) }
|
||||
var showHostMenu by rememberSaveable { mutableStateOf(false) }
|
||||
var showHostLeaveConfirm by rememberSaveable { mutableStateOf(false) }
|
||||
val isHost = accountViewModel.account.signer.pubKey == event.pubKey
|
||||
val leaveScope = rememberCoroutineScope()
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -256,7 +264,15 @@ internal fun AudioRoomFullScreen(
|
||||
OutlinedButton(onClick = { showReactionPicker = true }) {
|
||||
Text(stringRes(R.string.audio_room_reactions_button))
|
||||
}
|
||||
OutlinedButton(onClick = onLeave) {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
if (isHost) {
|
||||
showHostLeaveConfirm = true
|
||||
} else {
|
||||
onLeave()
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(stringRes(R.string.audio_room_leave))
|
||||
}
|
||||
}
|
||||
@@ -269,6 +285,44 @@ internal fun AudioRoomFullScreen(
|
||||
)
|
||||
}
|
||||
|
||||
if (showHostLeaveConfirm) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showHostLeaveConfirm = false },
|
||||
title = { Text(stringRes(R.string.audio_room_leave_host_title)) },
|
||||
text = { Text(stringRes(R.string.audio_room_leave_host_body)) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error),
|
||||
onClick = {
|
||||
showHostLeaveConfirm = false
|
||||
leaveScope.launch {
|
||||
val ok = closeMeetingSpace(accountViewModel, event)
|
||||
if (!ok) {
|
||||
accountViewModel.toastManager.toast(
|
||||
R.string.audio_rooms,
|
||||
R.string.audio_room_leave_host_close_failed,
|
||||
)
|
||||
}
|
||||
onLeave()
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(stringRes(R.string.audio_room_leave_host_close))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showHostLeaveConfirm = false
|
||||
onLeave()
|
||||
},
|
||||
) {
|
||||
Text(stringRes(R.string.audio_room_leave_host_just_leave))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
AudioRoomChatPanel(
|
||||
roomATag =
|
||||
ATag(
|
||||
@@ -498,6 +552,42 @@ private fun TalkRow(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-publish the host's [event] with `status="closed"`, preserving every
|
||||
* other field verbatim (room name, summary, image, service, endpoint,
|
||||
* participants). Returns true on success.
|
||||
*
|
||||
* Reuses [EditAudioRoomViewModel.buildEditTemplate] so the close-on-leave
|
||||
* path emits the same shape as the explicit Edit → Close room action.
|
||||
* Decoupled from [EditAudioRoomViewModel] state so the host's unsaved
|
||||
* edits in the Edit sheet (if any) cannot accidentally leak into the
|
||||
* close payload.
|
||||
*/
|
||||
private suspend fun closeMeetingSpace(
|
||||
accountViewModel: AccountViewModel,
|
||||
event: MeetingSpaceEvent,
|
||||
): Boolean {
|
||||
val verbatim =
|
||||
EditAudioRoomViewModel.FormState(
|
||||
dTag = event.dTag(),
|
||||
roomName = event.room().orEmpty(),
|
||||
summary = event.summary().orEmpty(),
|
||||
imageUrl = event.image().orEmpty(),
|
||||
endpointUrl = event.endpoint().orEmpty(),
|
||||
serviceUrl = event.service().orEmpty(),
|
||||
isPublishing = false,
|
||||
error = null,
|
||||
)
|
||||
return try {
|
||||
val template =
|
||||
EditAudioRoomViewModel.buildEditTemplate(event, verbatim, StatusTag.STATUS.CLOSED)
|
||||
accountViewModel.account.signAndComputeBroadcast(template)
|
||||
true
|
||||
} catch (_: Throwable) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an `nostr:naddr1...` URI for the room and hand it to the
|
||||
* system share sheet. Includes the room title in the share text
|
||||
|
||||
@@ -529,6 +529,11 @@
|
||||
<string name="audio_room_notification_stop">Stop</string>
|
||||
<string name="audio_room_join">Join audio room</string>
|
||||
<string name="audio_room_leave">Leave</string>
|
||||
<string name="audio_room_leave_host_title">End this audio room?</string>
|
||||
<string name="audio_room_leave_host_body">You\'re the host. Closing the room will disconnect everyone. Choose \"Just leave\" if you want to come back later — the room will auto-close after 8 hours of inactivity.</string>
|
||||
<string name="audio_room_leave_host_close">Close room</string>
|
||||
<string name="audio_room_leave_host_just_leave">Just leave</string>
|
||||
<string name="audio_room_leave_host_close_failed">Couldn\'t mark room as closed. Leaving anyway — room will auto-close.</string>
|
||||
<plurals name="audio_room_listener_count">
|
||||
<item quantity="one">%1$d listener</item>
|
||||
<item quantity="other">%1$d listeners</item>
|
||||
|
||||
Reference in New Issue
Block a user