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:
+2
@@ -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() {
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+16
@@ -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(
|
||||
|
||||
+31
-27
@@ -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<ParticipantTag>,
|
||||
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()
|
||||
}
|
||||
|
||||
+219
-224
@@ -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.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
|
||||
@@ -108,240 +107,237 @@ internal fun NestFullScreen(
|
||||
onHandRaisedChange: (Boolean) -> Unit,
|
||||
onLeave: () -> Unit,
|
||||
) {
|
||||
val roomTheme = 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) }
|
||||
var showReactionPicker by rememberSaveable { mutableStateOf(false) }
|
||||
var hostMenuTarget by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
// Tab selection survives configuration changes and PIP transitions.
|
||||
// Stored as ordinal so rememberSaveable can persist it without a
|
||||
// custom Saver.
|
||||
var selectedTabIndex by rememberSaveable { mutableStateOf(0) }
|
||||
var showEditSheet by rememberSaveable { mutableStateOf(false) }
|
||||
var showHostMenu by rememberSaveable { mutableStateOf(false) }
|
||||
var showHostLeaveConfirm by rememberSaveable { mutableStateOf(false) }
|
||||
var showReactionPicker by rememberSaveable { mutableStateOf(false) }
|
||||
var hostMenuTarget by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
// Tab selection survives configuration changes and PIP transitions.
|
||||
// Stored as ordinal so rememberSaveable can persist it without a
|
||||
// custom Saver.
|
||||
var selectedTabIndex by rememberSaveable { mutableStateOf(0) }
|
||||
|
||||
val isHost = accountViewModel.account.signer.pubKey == event.pubKey
|
||||
val myPubkey = accountViewModel.account.signer.pubKey
|
||||
val isOnStageMe = remember(onStage, myPubkey) { onStage.any { it.pubKey == myPubkey } }
|
||||
val leaveScope = rememberCoroutineScope()
|
||||
val topBarContext = LocalContext.current
|
||||
val isHost = accountViewModel.account.signer.pubKey == event.pubKey
|
||||
val myPubkey = accountViewModel.account.signer.pubKey
|
||||
val isOnStageMe = remember(onStage, myPubkey) { onStage.any { it.pubKey == myPubkey } }
|
||||
val leaveScope = rememberCoroutineScope()
|
||||
val topBarContext = LocalContext.current
|
||||
|
||||
val presences by viewModel.presences.collectAsState()
|
||||
val reactionsByPubkey by viewModel.recentReactions.collectAsState()
|
||||
val speakerCatalogs by viewModel.speakerCatalogs.collectAsState()
|
||||
val presences by viewModel.presences.collectAsState()
|
||||
val reactionsByPubkey by viewModel.recentReactions.collectAsState()
|
||||
val speakerCatalogs by viewModel.speakerCatalogs.collectAsState()
|
||||
|
||||
val onStageKeys = remember(onStage) { onStage.map { it.pubKey }.toSet() }
|
||||
val participantGrid =
|
||||
remember(event, presences) {
|
||||
buildParticipantGrid(
|
||||
participants = event.participants(),
|
||||
presences = presences,
|
||||
)
|
||||
val onStageKeys = remember(onStage) { onStage.map { it.pubKey }.toSet() }
|
||||
val participantGrid =
|
||||
remember(event, presences) {
|
||||
buildParticipantGrid(
|
||||
participants = event.participants(),
|
||||
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
|
||||
// section to the screen.
|
||||
val handsCount =
|
||||
remember(presences, onStageKeys) {
|
||||
presences.values.count { it.handRaised && it.pubkey !in onStageKeys }
|
||||
}
|
||||
val showHandsTab = isHost && handsCount > 0
|
||||
}
|
||||
val effectiveTab = tabs.getOrNull(selectedTabIndex) ?: NestTab.Chat
|
||||
|
||||
// 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)
|
||||
// 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(
|
||||
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
|
||||
},
|
||||
)
|
||||
},
|
||||
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(
|
||||
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
|
||||
},
|
||||
)
|
||||
},
|
||||
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 {
|
||||
// Sheets and dialogs render alongside the Scaffold so they
|
||||
// cover the room content (including the action bar) when
|
||||
// open. ParticipantHostActionsSheet stays outside the Column
|
||||
// for the same reason — modal bottom sheets are not affected
|
||||
// by parent layout.
|
||||
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()
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { 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),
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(stringRes(R.string.nest_leave_host_close))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showHostLeaveConfirm = false
|
||||
onLeave()
|
||||
},
|
||||
) {
|
||||
Text(stringRes(R.string.nest_leave_host_just_leave))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Sheets and dialogs render alongside the Scaffold so they
|
||||
// cover the room content (including the action bar) when
|
||||
// open. ParticipantHostActionsSheet stays outside the Column
|
||||
// for the same reason — modal bottom sheets are not affected
|
||||
// by parent layout.
|
||||
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 },
|
||||
)
|
||||
}
|
||||
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
|
||||
* triggered from here and rendered alongside the Scaffold.
|
||||
*
|
||||
* Container color is transparent so the themed background painted
|
||||
* by [NestThemedScope]'s Surface (and any optional `bg` image) shows
|
||||
* through cleanly.
|
||||
* Container color is transparent so the surface beneath the
|
||||
* Scaffold (set by the activity / parent theme) shows through.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
||||
@@ -479,6 +479,94 @@
|
||||
<string name="nest_chat_send_failed_title">无法发送信息</string>
|
||||
<string name="nest_no_app_to_open_link">没有安装打开此链接的应用。</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="articles">文章</string>
|
||||
<string name="private_bookmarks">私人书签</string>
|
||||
@@ -999,6 +1087,8 @@
|
||||
<string name="meeting_space_open_tag">开放</string>
|
||||
<string name="meeting_space_private_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="followed_tags">已关注的标签</string>
|
||||
<string name="relay_setup">中继器</string>
|
||||
@@ -1797,6 +1887,7 @@
|
||||
<string name="kind_profile_badges">个人资料徽章</string>
|
||||
<string name="kind_blocked_relays">中继黑名单</string>
|
||||
<string name="kind_blossom_servers">Blossom 服务器</string>
|
||||
<string name="kind_nests_servers">语音聊天服务器</string>
|
||||
<string name="kind_blossom_auth">Blossom 认证</string>
|
||||
<string name="kind_broadcast_relays">广播中继</string>
|
||||
<string name="kind_bookmark_list">书签列表</string>
|
||||
|
||||
@@ -1207,6 +1207,7 @@
|
||||
<string name="live_stream_offline_tag">OFFLINE</string>
|
||||
<string name="live_stream_ended_tag">ENDED</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_has_ended">Livestream Ended</string>
|
||||
|
||||
Reference in New Issue
Block a user