Merge branch 'main' into claude/fix-nest-activity-layout-zziPP

Resolves NestFullScreen.kt by keeping the redesigned layout (header
strip, stage grid, tabs, sticky NestActionBar) while dropping the
NestThemedScope wrapper that main removed in ff00dcf3 ("screens get
too dark"). The RoomTheme import goes with it; the stale doc-comment
reference to NestThemedScope is updated accordingly.
This commit is contained in:
Claude
2026-04-28 01:01:08 +00:00
9 changed files with 381 additions and 253 deletions
@@ -25,6 +25,7 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue import androidx.compose.material3.DrawerValue
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.ui.MainActivity import com.vitorpamplona.amethyst.ui.MainActivity
import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.Route
@@ -98,6 +99,7 @@ class BouncingIntentNav(
override fun navBottomBar(route: Route) = nav(route) override fun navBottomBar(route: Route) = nav(route)
@Composable
override fun canPop(): Boolean = false override fun canPop(): Boolean = false
override fun popBack() { override fun popBack() {
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.navigation.navs
import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue import androidx.compose.material3.DrawerValue
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -46,6 +47,7 @@ class EmptyNav : INav {
override fun navBottomBar(route: Route) {} override fun navBottomBar(route: Route) {}
@Composable
override fun canPop(): Boolean = false override fun canPop(): Boolean = false
override fun popBack() {} override fun popBack() {}
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.ui.navigation.navs package com.vitorpamplona.amethyst.ui.navigation.navs
import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -43,6 +44,7 @@ interface INav {
fun navBottomBar(route: Route) fun navBottomBar(route: Route)
@Composable
fun canPop(): Boolean fun canPop(): Boolean
fun popBack() fun popBack()
@@ -23,8 +23,12 @@ package com.vitorpamplona.amethyst.ui.navigation.navs
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue import androidx.compose.material3.DrawerValue
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController 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.BOTTOM_NAV_ROOT_KEY
import com.vitorpamplona.amethyst.ui.navigation.isBottomNavRoot import com.vitorpamplona.amethyst.ui.navigation.isBottomNavRoot
import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.Route
@@ -93,9 +97,20 @@ class Nav(
} }
} }
@Composable
override fun canPop(): Boolean { override fun canPop(): Boolean {
val current = controller.currentBackStackEntry ?: return false // Observe the current entry as State so consumers recompose when the
if (current.isBottomNavRoot()) return false // 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 return controller.previousBackStackEntry != null
} }
@@ -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 @Composable
fun OfflineFlag() { fun OfflineFlag() {
Text( Text(
@@ -39,6 +39,7 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment 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.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.EndedFlag 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.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.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.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.NestActivity
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.NestBridge import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.NestBridge
import com.vitorpamplona.amethyst.ui.stringRes 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.Size35dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdPadding 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.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 import kotlinx.collections.immutable.toImmutableList
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@@ -177,7 +180,8 @@ fun ObserveAndRenderSpace(
val card by observeNoteAndMap(baseNote, accountViewModel) { val card by observeNoteAndMap(baseNote, accountViewModel) {
when (val noteEvent = it.event) { when (val noteEvent = it.event) {
is MeetingSpaceEvent -> { is MeetingSpaceEvent -> {
LiveActivityCard( println("AABBCC ${noteEvent.address()} ${noteEvent.status()}")
NestCard(
id = noteEvent.address(), id = noteEvent.address(),
name = noteEvent.dTag(), name = noteEvent.dTag(),
cover = noteEvent.image()?.ifBlank { null }, cover = noteEvent.image()?.ifBlank { null },
@@ -185,13 +189,7 @@ fun ObserveAndRenderSpace(
subject = noteEvent.room()?.ifBlank { null }, subject = noteEvent.room()?.ifBlank { null },
content = noteEvent.summary(), content = noteEvent.summary(),
participants = noteEvent.participants().toImmutableList(), participants = noteEvent.participants().toImmutableList(),
status = status = noteEvent.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
},
starts = null, 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<ParticipantTag>,
val status: StatusTag.STATUS?,
val starts: Long?,
)
@Composable @Composable
fun RenderLiveSpacesThumb( fun RenderLiveSpacesThumb(
card: LiveActivityCard, card: NestCard,
baseNote: Note, baseNote: Note,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
@@ -244,22 +255,11 @@ fun RenderLiveSpacesThumb(
Box(Modifier.padding(10.dp)) { Box(Modifier.padding(10.dp)) {
CrossfadeIfEnabled(targetState = card.status, accountViewModel = accountViewModel) { CrossfadeIfEnabled(targetState = card.status, accountViewModel = accountViewModel) {
when (it) { when (it) {
StatusTag.STATUS.LIVE -> { StatusTag.STATUS.OPEN -> {
val url = card.media LiveFlag()
if (url.isNullOrBlank()) {
EndedFlag()
} else {
CheckIfVideoIsOnline(url, accountViewModel) { isOnline ->
if (isOnline) {
LiveFlag()
} else {
OfflineFlag()
}
}
}
} }
StatusTag.STATUS.ENDED -> { StatusTag.STATUS.CLOSED -> {
EndedFlag() EndedFlag()
} }
@@ -267,6 +267,10 @@ fun RenderLiveSpacesThumb(
ScheduledFlag(card.starts) ScheduledFlag(card.starts)
} }
StatusTag.STATUS.PRIVATE -> {
PrivateFlag()
}
else -> { else -> {
EndedFlag() EndedFlag()
} }
@@ -64,7 +64,6 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.viewmodels.NestUiState import com.vitorpamplona.amethyst.commons.viewmodels.NestUiState
import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel 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.commons.viewmodels.buildParticipantGrid
import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -108,240 +107,237 @@ internal fun NestFullScreen(
onHandRaisedChange: (Boolean) -> Unit, onHandRaisedChange: (Boolean) -> Unit,
onLeave: () -> Unit, onLeave: () -> Unit,
) { ) {
val roomTheme = remember(event) { RoomTheme.from(event) } var showEditSheet by rememberSaveable { mutableStateOf(false) }
NestThemedScope(theme = roomTheme, accountViewModel = accountViewModel) { var showHostMenu by rememberSaveable { mutableStateOf(false) }
var showEditSheet by rememberSaveable { mutableStateOf(false) } var showHostLeaveConfirm by rememberSaveable { mutableStateOf(false) }
var showHostMenu by rememberSaveable { mutableStateOf(false) } var showReactionPicker by rememberSaveable { mutableStateOf(false) }
var showHostLeaveConfirm by rememberSaveable { mutableStateOf(false) } var hostMenuTarget by rememberSaveable { mutableStateOf<String?>(null) }
var showReactionPicker by rememberSaveable { mutableStateOf(false) } // Tab selection survives configuration changes and PIP transitions.
var hostMenuTarget by rememberSaveable { mutableStateOf<String?>(null) } // Stored as ordinal so rememberSaveable can persist it without a
// Tab selection survives configuration changes and PIP transitions. // custom Saver.
// Stored as ordinal so rememberSaveable can persist it without a var selectedTabIndex by rememberSaveable { mutableStateOf(0) }
// custom Saver.
var selectedTabIndex by rememberSaveable { mutableStateOf(0) }
val isHost = accountViewModel.account.signer.pubKey == event.pubKey val isHost = accountViewModel.account.signer.pubKey == event.pubKey
val myPubkey = accountViewModel.account.signer.pubKey val myPubkey = accountViewModel.account.signer.pubKey
val isOnStageMe = remember(onStage, myPubkey) { onStage.any { it.pubKey == myPubkey } } val isOnStageMe = remember(onStage, myPubkey) { onStage.any { it.pubKey == myPubkey } }
val leaveScope = rememberCoroutineScope() val leaveScope = rememberCoroutineScope()
val topBarContext = LocalContext.current val topBarContext = LocalContext.current
val presences by viewModel.presences.collectAsState() val presences by viewModel.presences.collectAsState()
val reactionsByPubkey by viewModel.recentReactions.collectAsState() val reactionsByPubkey by viewModel.recentReactions.collectAsState()
val speakerCatalogs by viewModel.speakerCatalogs.collectAsState() val speakerCatalogs by viewModel.speakerCatalogs.collectAsState()
val onStageKeys = remember(onStage) { onStage.map { it.pubKey }.toSet() } val onStageKeys = remember(onStage) { onStage.map { it.pubKey }.toSet() }
val participantGrid = val participantGrid =
remember(event, presences) { remember(event, presences) {
buildParticipantGrid( buildParticipantGrid(
participants = event.participants(), participants = event.participants(),
presences = presences, presences = presences,
) )
}
// Same logic HandRaiseQueueSection uses internally — duplicated
// here so the tab label can show a count without coupling the
// section to the screen.
val handsCount =
remember(presences, onStageKeys) {
presences.values.count { it.handRaised && it.pubkey !in onStageKeys }
}
val showHandsTab = isHost && handsCount > 0
// Tab roster changes when the Hands tab appears/disappears.
// If the user was on Hands and the queue empties, fall back to
// Chat — kept as a stable default rather than Audience because
// chat is the room's primary engagement surface.
val tabs =
remember(showHandsTab) {
buildList {
add(NestTab.Chat)
add(NestTab.Audience)
if (showHandsTab) add(NestTab.Hands)
} }
// Same logic HandRaiseQueueSection uses internally — duplicated }
// here so the tab label can show a count without coupling the val effectiveTab = tabs.getOrNull(selectedTabIndex) ?: NestTab.Chat
// section to the screen.
val handsCount =
remember(presences, onStageKeys) {
presences.values.count { it.handRaised && it.pubkey !in onStageKeys }
}
val showHandsTab = isHost && handsCount > 0
// Tab roster changes when the Hands tab appears/disappears. // Long-press on any participant opens the host-actions sheet
// If the user was on Hands and the queue empties, fall back to // (T2 #2). The sheet itself gates which rows render based on
// Chat — kept as a stable default rather than Audience because // host status. Skip self.
// chat is the room's primary engagement surface. val onLongPressParticipant: ((String) -> Unit) = { target ->
val tabs = if (target != myPubkey) hostMenuTarget = target
remember(showHandsTab) { }
buildList {
add(NestTab.Chat) Scaffold(
add(NestTab.Audience) modifier = Modifier.fillMaxSize(),
if (showHandsTab) add(NestTab.Hands) 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
},
)
},
bottomBar = {
NestActionBar(
viewModel = viewModel,
ui = ui,
isOnStage = isOnStageMe,
canBroadcast = viewModel.canBroadcast,
speakerPubkeyHex = myPubkey,
handRaised = handRaised,
onHandRaisedChange = onHandRaisedChange,
onShowReactionPicker = { showReactionPicker = true },
onLeave = {
if (isHost) {
showHostLeaveConfirm = true
} else {
onLeave()
}
},
)
},
) { padding ->
Column(
modifier =
Modifier
.fillMaxSize()
.padding(padding),
) {
RoomHeaderStrip(
summary = event.summary(),
listenerCount = presences.size,
)
StageGrid(
members = participantGrid.onStage,
speakingNow = ui.speakingNow,
accountViewModel = accountViewModel,
reactionsByPubkey = reactionsByPubkey,
connectingSpeakers = ui.connectingSpeakers,
onLongPressParticipant = onLongPressParticipant,
modifier = Modifier.padding(horizontal = 16.dp),
)
NestTabRow(
tabs = tabs,
selectedTab = effectiveTab,
audienceCount = participantGrid.audience.size,
handsCount = handsCount,
onSelect = { tab -> selectedTabIndex = tabs.indexOf(tab).coerceAtLeast(0) },
modifier = Modifier.padding(top = 8.dp),
)
when (effectiveTab) {
NestTab.Chat -> {
NestChatPanel(
event = event,
viewModel = viewModel,
accountViewModel = accountViewModel,
modifier =
Modifier
.weight(1f)
.padding(horizontal = 16.dp),
)
}
NestTab.Audience -> {
AudienceGrid(
members = participantGrid.audience,
accountViewModel = accountViewModel,
onLongPressParticipant = onLongPressParticipant,
modifier =
Modifier
.weight(1f)
.padding(horizontal = 12.dp),
)
}
NestTab.Hands -> {
HandRaiseQueueSection(
event = event,
viewModel = viewModel,
accountViewModel = accountViewModel,
modifier =
Modifier
.weight(1f)
.padding(horizontal = 16.dp),
)
} }
} }
val effectiveTab = tabs.getOrNull(selectedTabIndex) ?: NestTab.Chat
// Long-press on any participant opens the host-actions sheet
// (T2 #2). The sheet itself gates which rows render based on
// host status. Skip self.
val onLongPressParticipant: ((String) -> Unit) = { target ->
if (target != myPubkey) hostMenuTarget = target
} }
}
Scaffold( // Sheets and dialogs render alongside the Scaffold so they
modifier = Modifier.fillMaxSize(), // cover the room content (including the action bar) when
containerColor = Color.Transparent, // open. ParticipantHostActionsSheet stays outside the Column
topBar = { // for the same reason — modal bottom sheets are not affected
NestTopAppBar( // by parent layout.
title = event.room().orEmpty(), hostMenuTarget?.let { target ->
isHost = isHost, ParticipantHostActionsSheet(
showHostMenu = showHostMenu, target = target,
onMenuOpen = { showHostMenu = true }, event = event,
onMenuDismiss = { showHostMenu = false }, accountViewModel = accountViewModel,
onShare = { onDismiss = { hostMenuTarget = null },
showHostMenu = false catalog = speakerCatalogs[target],
shareRoomNaddr(topBarContext, event) )
}, }
onEdit = {
showHostMenu = false if (showReactionPicker) {
showEditSheet = true RoomReactionPickerSheet(
}, onPick = { emoji -> accountViewModel.reactToOrDelete(roomNote, emoji) },
) onDismiss = { showReactionPicker = false },
}, )
bottomBar = { }
NestActionBar(
viewModel = viewModel, if (showHostLeaveConfirm) {
ui = ui, AlertDialog(
isOnStage = isOnStageMe, onDismissRequest = { showHostLeaveConfirm = false },
canBroadcast = viewModel.canBroadcast, title = { Text(stringRes(R.string.nest_leave_host_title)) },
speakerPubkeyHex = myPubkey, text = { Text(stringRes(R.string.nest_leave_host_body)) },
handRaised = handRaised, confirmButton = {
onHandRaisedChange = onHandRaisedChange, TextButton(
onShowReactionPicker = { showReactionPicker = true }, colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error),
onLeave = { onClick = {
if (isHost) { showHostLeaveConfirm = false
showHostLeaveConfirm = true leaveScope.launch {
} else { val ok = closeMeetingSpace(accountViewModel, event)
if (!ok) {
accountViewModel.toastManager.toast(
R.string.nests,
R.string.nest_leave_host_close_failed,
)
}
onLeave() onLeave()
} }
}, },
) ) {
}, Text(stringRes(R.string.nest_leave_host_close))
) { padding ->
Column(
modifier =
Modifier
.fillMaxSize()
.padding(padding),
) {
RoomHeaderStrip(
summary = event.summary(),
listenerCount = presences.size,
)
StageGrid(
members = participantGrid.onStage,
speakingNow = ui.speakingNow,
accountViewModel = accountViewModel,
reactionsByPubkey = reactionsByPubkey,
connectingSpeakers = ui.connectingSpeakers,
onLongPressParticipant = onLongPressParticipant,
modifier = Modifier.padding(horizontal = 16.dp),
)
NestTabRow(
tabs = tabs,
selectedTab = effectiveTab,
audienceCount = participantGrid.audience.size,
handsCount = handsCount,
onSelect = { tab -> selectedTabIndex = tabs.indexOf(tab).coerceAtLeast(0) },
modifier = Modifier.padding(top = 8.dp),
)
when (effectiveTab) {
NestTab.Chat -> {
NestChatPanel(
event = event,
viewModel = viewModel,
accountViewModel = accountViewModel,
modifier =
Modifier
.weight(1f)
.padding(horizontal = 16.dp),
)
}
NestTab.Audience -> {
AudienceGrid(
members = participantGrid.audience,
accountViewModel = accountViewModel,
onLongPressParticipant = onLongPressParticipant,
modifier =
Modifier
.weight(1f)
.padding(horizontal = 12.dp),
)
}
NestTab.Hands -> {
HandRaiseQueueSection(
event = event,
viewModel = viewModel,
accountViewModel = accountViewModel,
modifier =
Modifier
.weight(1f)
.padding(horizontal = 16.dp),
)
}
} }
} },
} dismissButton = {
TextButton(
onClick = {
showHostLeaveConfirm = false
onLeave()
},
) {
Text(stringRes(R.string.nest_leave_host_just_leave))
}
},
)
}
// Sheets and dialogs render alongside the Scaffold so they if (showEditSheet) {
// cover the room content (including the action bar) when EditNestSheet(
// open. ParticipantHostActionsSheet stays outside the Column accountViewModel = accountViewModel,
// for the same reason — modal bottom sheets are not affected event = event,
// by parent layout. onDismiss = { showEditSheet = false },
hostMenuTarget?.let { target -> )
ParticipantHostActionsSheet(
target = target,
event = event,
accountViewModel = accountViewModel,
onDismiss = { hostMenuTarget = null },
catalog = speakerCatalogs[target],
)
}
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))
}
},
)
}
if (showEditSheet) {
EditNestSheet(
accountViewModel = accountViewModel,
event = event,
onDismiss = { showEditSheet = false },
)
}
} }
} }
@@ -458,9 +454,8 @@ private enum class NestTab {
* state lives at the screen level so the EditNestSheet can be * state lives at the screen level so the EditNestSheet can be
* triggered from here and rendered alongside the Scaffold. * triggered from here and rendered alongside the Scaffold.
* *
* Container color is transparent so the themed background painted * Container color is transparent so the surface beneath the
* by [NestThemedScope]'s Surface (and any optional `bg` image) shows * Scaffold (set by the activity / parent theme) shows through.
* through cleanly.
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -479,6 +479,94 @@
<string name="nest_chat_send_failed_title">无法发送信息</string> <string name="nest_chat_send_failed_title">无法发送信息</string>
<string name="nest_no_app_to_open_link">没有安装打开此链接的应用。</string> <string name="nest_no_app_to_open_link">没有安装打开此链接的应用。</string>
<string name="nest_close_room_confirm_title">关闭这个聊天室吗?</string> <string name="nest_close_room_confirm_title">关闭这个聊天室吗?</string>
<string name="nest_close_room_confirm_body">所有参与者将被断开连接。该房间会在源中显示为已关闭的。</string>
<string name="nest_close_room_confirm_action">关闭聊天室</string>
<string name="nest_create_when_in_past">选择未来的开始时间。</string>
<string name="nest_audio_failed">音频失败: %1$s</string>
<string name="nest_audio_unavailable">此聊天室没有音频可用</string>
<string name="nest_talk">交谈</string>
<string name="nest_stop_talking">停止交谈</string>
<string name="nest_leave_stage">离开舞台</string>
<string name="nest_mic_mute">麦克风静音</string>
<string name="nest_mic_unmute">取消麦克风静音</string>
<string name="nest_broadcast_connecting">正在开始广播…</string>
<string name="nest_broadcasting">直播中</string>
<string name="nest_broadcast_failed">广播失败: %1$s</string>
<string name="nest_record_permission_required">在这个房间进行交谈需要麦克风权限。</string>
<string name="nest_open_settings">打开设置</string>
<string name="nest_notification_channel">音频室</string>
<string name="nest_notification_channel_description">当聊天室开放时保持音频播放。</string>
<string name="nest_notification_listening">已连接音频聊天室</string>
<string name="nest_notification_broadcasting">语音聊天室 — 在线</string>
<string name="nest_notification_text">轻触返回。</string>
<string name="nest_notification_stop">停止</string>
<string name="nest_join">加入语音聊天室</string>
<string name="nest_lobby_host_label">主持人</string>
<string name="nest_presence_raised_hand">举手</string>
<string name="nest_presence_on_stage">步入舞台</string>
<string name="nest_presence_speaking">正在说话</string>
<string name="nest_presence_listening">作为观众加入</string>
<string name="nest_presence_left">离开语音聊天室</string>
<string name="nest_leave">离开</string>
<string name="nest_leave_host_title">结束这个语音聊天室?</string>
<string name="nest_leave_host_body">您是主持人。关闭聊天室会断开所有人的连接。 如果您想稍后再回来,请选择\"仅离开\" — 该聊天室将在8小时不活动后自动关闭。</string>
<string name="nest_leave_host_close">关闭聊天室</string>
<string name="nest_leave_host_just_leave">仅离开</string>
<string name="nest_leave_host_close_failed">不能将聊天室标记为已关闭。仍然离开——聊天室将自动关闭。</string>
<plurals name="nest_listener_count">
<item quantity="other">%1$d 位听众</item>
</plurals>
<string name="nest_chat_send">发送</string>
<string name="nest_chat_placeholder">说些什么…</string>
<string name="nest_chat_empty">还没有消息。成为第一个说话的。</string>
<string name="nest_reactions_title">回应</string>
<string name="nest_reactions_button">回应</string>
<string name="nest_edit_title">编辑聊天室</string>
<string name="nest_edit_save">保存</string>
<string name="nest_close_action">关闭聊天室</string>
<string name="nest_overflow_menu">聊天室操作</string>
<string name="nest_hand_raise_queue_title">举手</string>
<string name="nest_hand_raise_approve">批准</string>
<string name="nest_promote_speaker">提升为演讲者</string>
<string name="nest_demote_listener">降级为听众</string>
<string name="nest_kick_action">踢出</string>
<string name="nest_participant_view_profile">查看个人资料</string>
<string name="nest_participant_zap">发送打闪</string>
<string name="nest_participant_zap_split_unsupported">不支持在聊天内分割打闪。打开个人资料屏发送打闪。</string>
<string name="nest_participant_follow">关注</string>
<string name="nest_participant_unfollow">取关</string>
<string name="nest_participant_mute">静音</string>
<string name="nest_participant_unmute">取消静音</string>
<string name="nest_share_action">分享聊天室</string>
<string name="nest_create_fab">启动空间</string>
<string name="nest_no_server_title">设置语音聊天室服务器</string>
<string name="nest_no_server_body">您尚未选择语音聊天服务器。添加 %1$s 到您的服务器列表并继续吗?\n\n您可以稍后在设置中更改此设置。</string>
<string name="nest_no_server_use_default">使用默认</string>
<string name="nest_no_server_cancel">取消</string>
<string name="nest_no_server_save_failed">无法保存您的语音聊天服务器列表。请重试。</string>
<string name="nest_create_title">启动新的语音聊天室</string>
<string name="nest_create_field_room">聊天室名称</string>
<string name="nest_create_field_summary">这是什么?</string>
<string name="nest_create_field_service">MoQ 服务 URL</string>
<string name="nest_create_field_service_hint">Auth sidecar —— 默认 nostrnests.com</string>
<string name="nest_create_field_endpoint">MoQ 中继端点</string>
<string name="nest_create_field_endpoint_hint">WebTransport URL - 通常是同一个主机</string>
<string name="nest_create_field_image">封面图片URL (可选)</string>
<string name="nest_create_cancel">取消</string>
<string name="nest_create_submit">启动空间</string>
<string name="nest_create_schedule_toggle">计划稍后进行</string>
<string name="nest_create_when">选择开始时间</string>
<string name="nests_servers_title">语音聊天服务器</string>
<string name="nests_servers_explainer">选择 Amethyst 将你的语音聊天室发布到哪些 MoQ 主机服务器。启动新空间时第一个条目将用作默认。</string>
<string name="nests_servers_my_section">您的服务器</string>
<string name="nests_servers_my_explainer">保存为 10112 类型的可替换事件,以便其他客户端可以读取您的首选项。</string>
<string name="nests_servers_add_field">添加语音聊天服务器</string>
<string name="nests_servers_recommended_section">推荐的服务器</string>
<string name="nests_servers_recommended_explainer">内置建议,您可轻触添加到列表中。</string>
<string name="nests_servers_use_defaults">使用 Amethyst 默认值</string>
<string name="nests_servers_add_recommended">添加服务器</string>
<string name="nests_servers_remove">移除服务器</string>
<string name="nests_servers_empty">暂无语音聊天服务器。在下方添加一个或选择推荐服务器。</string>
<string name="longs">视频</string> <string name="longs">视频</string>
<string name="articles">文章</string> <string name="articles">文章</string>
<string name="private_bookmarks">私人书签</string> <string name="private_bookmarks">私人书签</string>
@@ -999,6 +1087,8 @@
<string name="meeting_space_open_tag">开放</string> <string name="meeting_space_open_tag">开放</string>
<string name="meeting_space_private_tag">私密</string> <string name="meeting_space_private_tag">私密</string>
<string name="meeting_space_closed_tag">关闭</string> <string name="meeting_space_closed_tag">关闭</string>
<string name="meeting_space_planned_tag">已计划</string>
<string name="meeting_space_planned_starts_at">启动 %1$s</string>
<string name="are_you_sure_you_want_to_log_out">登出将删除你的本地信息。 请确保备份你的私钥以避免失去你的帐户。你想要继续吗?</string> <string name="are_you_sure_you_want_to_log_out">登出将删除你的本地信息。 请确保备份你的私钥以避免失去你的帐户。你想要继续吗?</string>
<string name="followed_tags">已关注的标签</string> <string name="followed_tags">已关注的标签</string>
<string name="relay_setup">中继器</string> <string name="relay_setup">中继器</string>
@@ -1797,6 +1887,7 @@
<string name="kind_profile_badges">个人资料徽章</string> <string name="kind_profile_badges">个人资料徽章</string>
<string name="kind_blocked_relays">中继黑名单</string> <string name="kind_blocked_relays">中继黑名单</string>
<string name="kind_blossom_servers">Blossom 服务器</string> <string name="kind_blossom_servers">Blossom 服务器</string>
<string name="kind_nests_servers">语音聊天服务器</string>
<string name="kind_blossom_auth">Blossom 认证</string> <string name="kind_blossom_auth">Blossom 认证</string>
<string name="kind_broadcast_relays">广播中继</string> <string name="kind_broadcast_relays">广播中继</string>
<string name="kind_bookmark_list">书签列表</string> <string name="kind_bookmark_list">书签列表</string>
+1
View File
@@ -1207,6 +1207,7 @@
<string name="live_stream_offline_tag">OFFLINE</string> <string name="live_stream_offline_tag">OFFLINE</string>
<string name="live_stream_ended_tag">ENDED</string> <string name="live_stream_ended_tag">ENDED</string>
<string name="live_stream_planned_tag">SCHEDULED</string> <string name="live_stream_planned_tag">SCHEDULED</string>
<string name="live_stream_private_tag">PRIVATE</string>
<string name="live_stream_is_offline">Livestream is Offline</string> <string name="live_stream_is_offline">Livestream is Offline</string>
<string name="live_stream_has_ended">Livestream Ended</string> <string name="live_stream_has_ended">Livestream Ended</string>