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.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.AssistChip
|
import androidx.compose.material3.AssistChip
|
||||||
import androidx.compose.material3.AssistChipDefaults
|
import androidx.compose.material3.AssistChipDefaults
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.FilledTonalIconToggleButton
|
import androidx.compose.material3.FilledTonalIconToggleButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
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.nip01Core.tags.aTag.ATag
|
||||||
import com.vitorpamplona.quartz.nip19Bech32.toNAddr
|
import com.vitorpamplona.quartz.nip19Bech32.toNAddr
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
||||||
|
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.StatusTag
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag
|
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Full-screen layout for [AudioRoomActivity]. Renders title + summary,
|
* Full-screen layout for [AudioRoomActivity]. Renders title + summary,
|
||||||
@@ -98,7 +104,9 @@ internal fun AudioRoomFullScreen(
|
|||||||
) {
|
) {
|
||||||
var showEditSheet by rememberSaveable { mutableStateOf(false) }
|
var showEditSheet by rememberSaveable { mutableStateOf(false) }
|
||||||
var showHostMenu 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 isHost = accountViewModel.account.signer.pubKey == event.pubKey
|
||||||
|
val leaveScope = rememberCoroutineScope()
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -256,7 +264,15 @@ internal fun AudioRoomFullScreen(
|
|||||||
OutlinedButton(onClick = { showReactionPicker = true }) {
|
OutlinedButton(onClick = { showReactionPicker = true }) {
|
||||||
Text(stringRes(R.string.audio_room_reactions_button))
|
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))
|
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(
|
AudioRoomChatPanel(
|
||||||
roomATag =
|
roomATag =
|
||||||
ATag(
|
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
|
* Build an `nostr:naddr1...` URI for the room and hand it to the
|
||||||
* system share sheet. Includes the room title in the share text
|
* 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_notification_stop">Stop</string>
|
||||||
<string name="audio_room_join">Join audio room</string>
|
<string name="audio_room_join">Join audio room</string>
|
||||||
<string name="audio_room_leave">Leave</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">
|
<plurals name="audio_room_listener_count">
|
||||||
<item quantity="one">%1$d listener</item>
|
<item quantity="one">%1$d listener</item>
|
||||||
<item quantity="other">%1$d listeners</item>
|
<item quantity="other">%1$d listeners</item>
|
||||||
|
|||||||
Reference in New Issue
Block a user