diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/audiorooms/room/AudioRoomFullScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/audiorooms/room/AudioRoomFullScreen.kt index 41a99ca97..de7f0d7af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/audiorooms/room/AudioRoomFullScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/audiorooms/room/AudioRoomFullScreen.kt @@ -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 diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index cd621dc77..c4565defc 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -529,6 +529,11 @@ Stop Join audio room Leave + End this audio room? + 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. + Close room + Just leave + Couldn\'t mark room as closed. Leaving anyway — room will auto-close. %1$d listener %1$d listeners