From 078c5f04a89dea9d222eb38bd5de352fc5b19236 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 27 Apr 2026 20:35:40 -0400 Subject: [PATCH 1/4] Solving the issue of a BackArrow on the Home Screen after a back press --- .../ui/navigation/navs/BouncingIntentNav.kt | 2 ++ .../amethyst/ui/navigation/navs/EmptyNav.kt | 2 ++ .../amethyst/ui/navigation/navs/INav.kt | 2 ++ .../amethyst/ui/navigation/navs/Nav.kt | 19 +++++++++++++++++-- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/BouncingIntentNav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/BouncingIntentNav.kt index d441417a4..a345ed112 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/BouncingIntentNav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/BouncingIntentNav.kt @@ -25,6 +25,7 @@ import android.content.Intent import android.net.Uri import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerValue +import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.MainActivity import com.vitorpamplona.amethyst.ui.navigation.routes.Route @@ -98,6 +99,7 @@ class BouncingIntentNav( override fun navBottomBar(route: Route) = nav(route) + @Composable override fun canPop(): Boolean = false override fun popBack() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/EmptyNav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/EmptyNav.kt index 06c7edab7..94c3f86a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/EmptyNav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/EmptyNav.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.navigation.navs import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerValue +import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.navigation.routes.Route import kotlinx.coroutines.CoroutineScope @@ -46,6 +47,7 @@ class EmptyNav : INav { override fun navBottomBar(route: Route) {} + @Composable override fun canPop(): Boolean = false override fun popBack() {} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/INav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/INav.kt index 7ec762f8e..f7ceff737 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/INav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/INav.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.navigation.navs import androidx.compose.material3.DrawerState +import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.navigation.routes.Route import kotlinx.coroutines.CoroutineScope @@ -43,6 +44,7 @@ interface INav { fun navBottomBar(route: Route) + @Composable fun canPop(): Boolean fun popBack() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt index 3d2a94b4f..05ca546fd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt @@ -23,8 +23,12 @@ package com.vitorpamplona.amethyst.ui.navigation.navs import android.annotation.SuppressLint import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerValue +import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController +import androidx.navigation.compose.currentBackStackEntryAsState import com.vitorpamplona.amethyst.ui.navigation.BOTTOM_NAV_ROOT_KEY import com.vitorpamplona.amethyst.ui.navigation.isBottomNavRoot import com.vitorpamplona.amethyst.ui.navigation.routes.Route @@ -93,9 +97,20 @@ class Nav( } } + @Composable override fun canPop(): Boolean { - val current = controller.currentBackStackEntry ?: return false - if (current.isBottomNavRoot()) return false + // Observe the current entry as State so consumers recompose when the + // back stack settles after a navigation or back-swipe transition. + // A non-reactive read would leave a stale value behind: e.g. on + // back-swipe to Home, previousBackStackEntry is still the popping + // entry until the gesture finishes, and nothing would re-evaluate + // canPop afterwards. + val current by controller.currentBackStackEntryAsState() + val entry = current ?: return false + if (entry.isBottomNavRoot()) return false + // Home is the graph's start destination and nothing can sit below + // it, so a back arrow there is never meaningful. + if (entry.destination.id == controller.graph.findStartDestination().id) return false return controller.previousBackStackEntry != null } From 9d0c224c9e387316aa901e8e02702b0f0a24c733 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 27 Apr 2026 20:36:45 -0400 Subject: [PATCH 2/4] New Private FLAG for Nests --- .../StreamingStatusFlags.kt | 16 +++++ .../screen/loggedIn/nests/NestsFeedLoaded.kt | 58 ++++++++++--------- amethyst/src/main/res/values/strings.xml | 1 + 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/StreamingStatusFlags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/StreamingStatusFlags.kt index 86b035d5d..f76ba3080 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/StreamingStatusFlags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/StreamingStatusFlags.kt @@ -73,6 +73,22 @@ fun EndedFlag() { ) } +@Composable +fun PrivateFlag() { + Text( + text = stringRes(id = R.string.live_stream_private_tag), + color = Color.White, + fontWeight = FontWeight.Bold, + modifier = + remember { + Modifier + .clip(SmallBorder) + .background(Color.Black) + .padding(horizontal = 5.dp) + }, + ) +} + @Composable fun OfflineFlag() { Text( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt index 436fbf0cf..3bcc58b87 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt @@ -39,6 +39,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -70,11 +71,9 @@ import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.EndedFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveFlag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.OfflineFlag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.PrivateFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ScheduledFlag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.LiveActivityCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.LoadParticipants -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfVideoIsOnline import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.NestActivity import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.NestBridge import com.vitorpamplona.amethyst.ui.stringRes @@ -87,8 +86,12 @@ import com.vitorpamplona.amethyst.ui.theme.Size34dp import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdPadding +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag +import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.StatusTag +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @OptIn(ExperimentalFoundationApi::class) @@ -177,7 +180,8 @@ fun ObserveAndRenderSpace( val card by observeNoteAndMap(baseNote, accountViewModel) { when (val noteEvent = it.event) { is MeetingSpaceEvent -> { - LiveActivityCard( + println("AABBCC ${noteEvent.address()} ${noteEvent.status()}") + NestCard( id = noteEvent.address(), name = noteEvent.dTag(), cover = noteEvent.image()?.ifBlank { null }, @@ -185,13 +189,7 @@ fun ObserveAndRenderSpace( subject = noteEvent.room()?.ifBlank { null }, content = noteEvent.summary(), participants = noteEvent.participants().toImmutableList(), - status = - when (noteEvent.status()) { - com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.StatusTag.STATUS.OPEN -> StatusTag.STATUS.LIVE - com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.StatusTag.STATUS.PRIVATE -> StatusTag.STATUS.PLANNED - com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.StatusTag.STATUS.CLOSED -> StatusTag.STATUS.ENDED - else -> null - }, + status = noteEvent.status(), starts = null, ) } @@ -212,9 +210,22 @@ fun ObserveAndRenderSpace( } } +@Immutable +data class NestCard( + val id: Address?, + val name: String, + val cover: String?, + val media: String?, + val subject: String?, + val content: String?, + val participants: ImmutableList, + val status: StatusTag.STATUS?, + val starts: Long?, +) + @Composable fun RenderLiveSpacesThumb( - card: LiveActivityCard, + card: NestCard, baseNote: Note, accountViewModel: AccountViewModel, nav: INav, @@ -244,22 +255,11 @@ fun RenderLiveSpacesThumb( Box(Modifier.padding(10.dp)) { CrossfadeIfEnabled(targetState = card.status, accountViewModel = accountViewModel) { when (it) { - StatusTag.STATUS.LIVE -> { - val url = card.media - if (url.isNullOrBlank()) { - EndedFlag() - } else { - CheckIfVideoIsOnline(url, accountViewModel) { isOnline -> - if (isOnline) { - LiveFlag() - } else { - OfflineFlag() - } - } - } + StatusTag.STATUS.OPEN -> { + LiveFlag() } - StatusTag.STATUS.ENDED -> { + StatusTag.STATUS.CLOSED -> { EndedFlag() } @@ -267,6 +267,10 @@ fun RenderLiveSpacesThumb( ScheduledFlag(card.starts) } + StatusTag.STATUS.PRIVATE -> { + PrivateFlag() + } + else -> { EndedFlag() } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 1807156d0..a6c3c5d54 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1202,6 +1202,7 @@ OFFLINE ENDED SCHEDULED + PRIVATE Livestream is Offline Livestream Ended From ff00dcf3c62ac64c7d8cfdf11311c30c60e319dc Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 27 Apr 2026 20:38:18 -0400 Subject: [PATCH 3/4] Removes the implementation of custom designs because our screens get too dark --- .../loggedIn/nests/room/NestFullScreen.kt | 416 +++++++++--------- 1 file changed, 206 insertions(+), 210 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/NestFullScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/NestFullScreen.kt index c5356a2c7..12e5cf79e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/NestFullScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/NestFullScreen.kt @@ -70,7 +70,6 @@ import com.vitorpamplona.amethyst.commons.viewmodels.BroadcastUiState import com.vitorpamplona.amethyst.commons.viewmodels.ConnectionUiState import com.vitorpamplona.amethyst.commons.viewmodels.NestUiState import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel -import com.vitorpamplona.amethyst.commons.viewmodels.RoomTheme import com.vitorpamplona.amethyst.commons.viewmodels.buildParticipantGrid import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -103,228 +102,225 @@ internal fun NestFullScreen( onHandRaisedChange: (Boolean) -> Unit, onLeave: () -> Unit, ) { - val roomTheme = androidx.compose.runtime.remember(event) { RoomTheme.from(event) } - NestThemedScope(theme = roomTheme, accountViewModel = accountViewModel) { - 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() - val topBarContext = LocalContext.current + 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() + val topBarContext = LocalContext.current - // Scaffold owns safeDrawing insets via its `contentWindowInsets` - // default, so we don't manage them manually anymore. The - // container is transparent because NestThemedScope's outer - // Surface already paints the themed background (and overlays - // the optional `bg` image on top of it); painting again here - // would double up. - Scaffold( - modifier = Modifier.fillMaxSize(), - containerColor = Color.Transparent, - topBar = { - NestTopAppBar( - title = event.room().orEmpty(), - isHost = isHost, - showHostMenu = showHostMenu, - onMenuOpen = { showHostMenu = true }, - onMenuDismiss = { showHostMenu = false }, - onShare = { - showHostMenu = false - shareRoomNaddr(topBarContext, event) - }, - onEdit = { - showHostMenu = false - showEditSheet = true - }, - ) - }, - ) { padding -> - // Inner column is just the two weighted siblings — top - // metadata (scrolls internally if overflow) and the chat - // panel (takes the rest). Title and overflow menu live in - // the TopAppBar above. + // Scaffold owns safeDrawing insets via its `contentWindowInsets` + // default, so we don't manage them manually anymore. The + // container is transparent because NestThemedScope's outer + // Surface already paints the themed background (and overlays + // the optional `bg` image on top of it); painting again here + // would double up. + Scaffold( + modifier = Modifier.fillMaxSize(), + containerColor = Color.Transparent, + topBar = { + NestTopAppBar( + title = event.room().orEmpty(), + isHost = isHost, + showHostMenu = showHostMenu, + onMenuOpen = { showHostMenu = true }, + onMenuDismiss = { showHostMenu = false }, + onShare = { + showHostMenu = false + shareRoomNaddr(topBarContext, event) + }, + onEdit = { + showHostMenu = false + showEditSheet = true + }, + ) + }, + ) { padding -> + // Inner column is just the two weighted siblings — top + // metadata (scrolls internally if overflow) and the chat + // panel (takes the rest). Title and overflow menu live in + // the TopAppBar above. + Column( + modifier = + Modifier + .fillMaxSize() + .padding(padding), + ) { Column( modifier = Modifier - .fillMaxSize() - .padding(padding), + .weight(1f) + .verticalScroll(rememberScrollState()) + .padding(horizontal = 16.dp), ) { - Column( - modifier = - Modifier - .weight(1f) - .verticalScroll(rememberScrollState()) - .padding(horizontal = 16.dp), - ) { - event.summary()?.let { - Text( - text = it, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - - // Listener counter — counts every active kind-10312 presence - // in the room. Hidden until the aggregator has at least one - // entry so the placeholder doesn't flash on entry. - val presences by viewModel.presences.collectAsState() - - val reactionsByPubkey by viewModel.recentReactions.collectAsState() - var hostMenuTarget by rememberSaveable { mutableStateOf(null) } - // Long-press opens the participant context sheet for ANYONE - // (T2 #2). The sheet's own gating decides which rows to show - // (follow/mute always; promote/demote/kick host-only). - val onLongPressParticipant: ((String) -> Unit) = { target -> - if (target != accountViewModel.account.signer.pubKey) hostMenuTarget = target - } - // Tier-2 #1: replace the two LazyRow sections with a single - // pure-projection ParticipantGrid. The `absent` flag (member - // promoted in the kind-30312 but never emitted a kind-10312) - // greys out at 50 % alpha, matching nostrnests' web client. - val participantGrid = - androidx.compose.runtime.remember(event, presences) { - buildParticipantGrid( - participants = event.participants(), - presences = presences, - ) - } - ParticipantsGrid( - grid = participantGrid, - speakingNow = ui.speakingNow, - accountViewModel = accountViewModel, - onStageLabel = stringRes(R.string.nest_stage), - audienceLabel = stringRes(R.string.nest_audience), - reactionsByPubkey = reactionsByPubkey, - connectingSpeakers = ui.connectingSpeakers, - onLongPressParticipant = onLongPressParticipant, + event.summary()?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) - val speakerCatalogs by viewModel.speakerCatalogs.collectAsState() - hostMenuTarget?.let { target -> - ParticipantHostActionsSheet( - target = target, - event = event, - accountViewModel = accountViewModel, - onDismiss = { hostMenuTarget = null }, - catalog = speakerCatalogs[target], - ) - } - - if (isHost) { - HandRaiseQueueSection( - event = event, - viewModel = viewModel, - accountViewModel = accountViewModel, - ) - } - - ConnectionRow(viewModel = viewModel, ui = ui) - - val myPubkey = accountViewModel.account.signer.pubKey - if (viewModel.canBroadcast && onStage.any { it.pubKey == myPubkey }) { - TalkRow(viewModel = viewModel, ui = ui, speakerPubkeyHex = myPubkey) - } - - var showReactionPicker by rememberSaveable { mutableStateOf(false) } - Row( - modifier = Modifier.fillMaxWidth().padding(top = 8.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - FilledTonalIconToggleButton( - checked = handRaised, - onCheckedChange = onHandRaisedChange, - ) { - Icon( - symbol = MaterialSymbols.PanTool, - contentDescription = - stringRes( - if (handRaised) R.string.nest_lower_hand else R.string.nest_raise_hand, - ), - ) - } - OutlinedButton(onClick = { showReactionPicker = true }) { - Text(stringRes(R.string.nest_reactions_button)) - } - OutlinedButton( - onClick = { - if (isHost) { - showHostLeaveConfirm = true - } else { - onLeave() - } - }, - ) { - Text(stringRes(R.string.nest_leave)) - } - } - if (showReactionPicker) { - RoomReactionPickerSheet( - onPick = { emoji -> - accountViewModel.reactToOrDelete(roomNote, emoji) - }, - onDismiss = { showReactionPicker = false }, - ) - } - - if (showHostLeaveConfirm) { - AlertDialog( - onDismissRequest = { showHostLeaveConfirm = false }, - title = { Text(stringRes(R.string.nest_leave_host_title)) }, - text = { Text(stringRes(R.string.nest_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.nests, - R.string.nest_leave_host_close_failed, - ) - } - onLeave() - } - }, - ) { - Text(stringRes(R.string.nest_leave_host_close)) - } - }, - dismissButton = { - TextButton( - onClick = { - showHostLeaveConfirm = false - onLeave() - }, - ) { - Text(stringRes(R.string.nest_leave_host_just_leave)) - } - }, - ) - } } - NestChatPanel( - event = event, - viewModel = viewModel, - accountViewModel = accountViewModel, - modifier = Modifier.weight(1f), - ) - } - } + // Listener counter — counts every active kind-10312 presence + // in the room. Hidden until the aggregator has at least one + // entry so the placeholder doesn't flash on entry. + val presences by viewModel.presences.collectAsState() - // EditNestSheet renders as a ModalBottomSheet — placement in - // the tree doesn't affect layout, but keeping it adjacent to - // the Scaffold makes the dialog/sheet boundary obvious. - if (showEditSheet) { - EditNestSheet( - accountViewModel = accountViewModel, + val reactionsByPubkey by viewModel.recentReactions.collectAsState() + var hostMenuTarget by rememberSaveable { mutableStateOf(null) } + // Long-press opens the participant context sheet for ANYONE + // (T2 #2). The sheet's own gating decides which rows to show + // (follow/mute always; promote/demote/kick host-only). + val onLongPressParticipant: ((String) -> Unit) = { target -> + if (target != accountViewModel.account.signer.pubKey) hostMenuTarget = target + } + // Tier-2 #1: replace the two LazyRow sections with a single + // pure-projection ParticipantGrid. The `absent` flag (member + // promoted in the kind-30312 but never emitted a kind-10312) + // greys out at 50 % alpha, matching nostrnests' web client. + val participantGrid = + androidx.compose.runtime.remember(event, presences) { + buildParticipantGrid( + participants = event.participants(), + presences = presences, + ) + } + ParticipantsGrid( + grid = participantGrid, + speakingNow = ui.speakingNow, + accountViewModel = accountViewModel, + onStageLabel = stringRes(R.string.nest_stage), + audienceLabel = stringRes(R.string.nest_audience), + reactionsByPubkey = reactionsByPubkey, + connectingSpeakers = ui.connectingSpeakers, + onLongPressParticipant = onLongPressParticipant, + ) + val speakerCatalogs by viewModel.speakerCatalogs.collectAsState() + hostMenuTarget?.let { target -> + ParticipantHostActionsSheet( + target = target, + event = event, + accountViewModel = accountViewModel, + onDismiss = { hostMenuTarget = null }, + catalog = speakerCatalogs[target], + ) + } + + if (isHost) { + HandRaiseQueueSection( + event = event, + viewModel = viewModel, + accountViewModel = accountViewModel, + ) + } + + ConnectionRow(viewModel = viewModel, ui = ui) + + val myPubkey = accountViewModel.account.signer.pubKey + if (viewModel.canBroadcast && onStage.any { it.pubKey == myPubkey }) { + TalkRow(viewModel = viewModel, ui = ui, speakerPubkeyHex = myPubkey) + } + + var showReactionPicker by rememberSaveable { mutableStateOf(false) } + Row( + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + FilledTonalIconToggleButton( + checked = handRaised, + onCheckedChange = onHandRaisedChange, + ) { + Icon( + symbol = MaterialSymbols.PanTool, + contentDescription = + stringRes( + if (handRaised) R.string.nest_lower_hand else R.string.nest_raise_hand, + ), + ) + } + OutlinedButton(onClick = { showReactionPicker = true }) { + Text(stringRes(R.string.nest_reactions_button)) + } + OutlinedButton( + onClick = { + if (isHost) { + showHostLeaveConfirm = true + } else { + onLeave() + } + }, + ) { + Text(stringRes(R.string.nest_leave)) + } + } + if (showReactionPicker) { + RoomReactionPickerSheet( + onPick = { emoji -> + accountViewModel.reactToOrDelete(roomNote, emoji) + }, + onDismiss = { showReactionPicker = false }, + ) + } + + if (showHostLeaveConfirm) { + AlertDialog( + onDismissRequest = { showHostLeaveConfirm = false }, + title = { Text(stringRes(R.string.nest_leave_host_title)) }, + text = { Text(stringRes(R.string.nest_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.nests, + R.string.nest_leave_host_close_failed, + ) + } + onLeave() + } + }, + ) { + Text(stringRes(R.string.nest_leave_host_close)) + } + }, + dismissButton = { + TextButton( + onClick = { + showHostLeaveConfirm = false + onLeave() + }, + ) { + Text(stringRes(R.string.nest_leave_host_just_leave)) + } + }, + ) + } + } + + NestChatPanel( event = event, - onDismiss = { showEditSheet = false }, + viewModel = viewModel, + accountViewModel = accountViewModel, + modifier = Modifier.weight(1f), ) } } + + // EditNestSheet renders as a ModalBottomSheet — placement in + // the tree doesn't affect layout, but keeping it adjacent to + // the Scaffold makes the dialog/sheet boundary obvious. + if (showEditSheet) { + EditNestSheet( + accountViewModel = accountViewModel, + event = event, + onDismiss = { showEditSheet = false }, + ) + } } /** From d7c6e9b169e6bb363c4fcca432df18e75fc82297 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 28 Apr 2026 00:40:23 +0000 Subject: [PATCH 4/4] New Crowdin translations by GitHub Action --- .../src/main/res/values-zh-rCN/strings.xml | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index 6703efbc4..cf4fd906d 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -479,6 +479,94 @@ 无法发送信息 没有安装打开此链接的应用。 关闭这个聊天室吗? + 所有参与者将被断开连接。该房间会在源中显示为已关闭的。 + 关闭聊天室 + 选择未来的开始时间。 + 音频失败: %1$s + 此聊天室没有音频可用 + 交谈 + 停止交谈 + 离开舞台 + 麦克风静音 + 取消麦克风静音 + 正在开始广播… + 直播中 + 广播失败: %1$s + 在这个房间进行交谈需要麦克风权限。 + 打开设置 + 音频室 + 当聊天室开放时保持音频播放。 + 已连接音频聊天室 + 语音聊天室 — 在线 + 轻触返回。 + 停止 + 加入语音聊天室 + 主持人 + 举手 + 步入舞台 + 正在说话 + 作为观众加入 + 离开语音聊天室 + 离开 + 结束这个语音聊天室? + 您是主持人。关闭聊天室会断开所有人的连接。 如果您想稍后再回来,请选择\"仅离开\" — 该聊天室将在8小时不活动后自动关闭。 + 关闭聊天室 + 仅离开 + 不能将聊天室标记为已关闭。仍然离开——聊天室将自动关闭。 + + %1$d 位听众 + + 发送 + 说些什么… + 还没有消息。成为第一个说话的。 + 回应 + 回应 + 编辑聊天室 + 保存 + 关闭聊天室 + 聊天室操作 + 举手 + 批准 + 提升为演讲者 + 降级为听众 + 踢出 + 查看个人资料 + 发送打闪 + 不支持在聊天内分割打闪。打开个人资料屏发送打闪。 + 关注 + 取关 + 静音 + 取消静音 + 分享聊天室 + 启动空间 + 设置语音聊天室服务器 + 您尚未选择语音聊天服务器。添加 %1$s 到您的服务器列表并继续吗?\n\n您可以稍后在设置中更改此设置。 + 使用默认 + 取消 + 无法保存您的语音聊天服务器列表。请重试。 + 启动新的语音聊天室 + 聊天室名称 + 这是什么? + MoQ 服务 URL + Auth sidecar —— 默认 nostrnests.com + MoQ 中继端点 + WebTransport URL - 通常是同一个主机 + 封面图片URL (可选) + 取消 + 启动空间 + 计划稍后进行 + 选择开始时间 + 语音聊天服务器 + 选择 Amethyst 将你的语音聊天室发布到哪些 MoQ 主机服务器。启动新空间时第一个条目将用作默认。 + 您的服务器 + 保存为 10112 类型的可替换事件,以便其他客户端可以读取您的首选项。 + 添加语音聊天服务器 + 推荐的服务器 + 内置建议,您可轻触添加到列表中。 + 使用 Amethyst 默认值 + 添加服务器 + 移除服务器 + 暂无语音聊天服务器。在下方添加一个或选择推荐服务器。 视频 文章 私人书签 @@ -999,6 +1087,8 @@ 开放 私密 关闭 + 已计划 + 启动 %1$s 登出将删除你的本地信息。 请确保备份你的私钥以避免失去你的帐户。你想要继续吗? 已关注的标签 中继器 @@ -1797,6 +1887,7 @@ 个人资料徽章 中继黑名单 Blossom 服务器 + 语音聊天服务器 Blossom 认证 广播中继 书签列表