fix(nests): UI polish — Stop button, unjoinable state, hand queue, DST
Audit-driven UI fixes that don't fit the perf bucket: * NestActionBar — wire StopBroadcastButton between MicMuteToggle and LeaveStageButton while broadcasting. The doc table at the top of the file already advertised [Mute] [Stop] [Leave the Stage] but the button was defined and never used; speakers had to leave the stage to stop sending audio. * NestActivityContent — render an explicit loading spinner while the kind-30312 resolves and a clear "this room can't be opened" page with a Back button when the event is missing service/endpoint. Replaces the previous black-screen dead end. * HandRaiseQueueSection — wrap filter+sort in remember so a heartbeat doesn't rebuild the queue on every recompose, swap to LazyColumn so a long queue doesn't render every row every frame, and show UsernameDisplay instead of the raw 8-char hex prefix. * CreateNestSheet schedule picker — compute the timezone offset at the picked instant rather than at Instant.now(); the previous variant was off by one hour for rooms scheduled across a DST transition. * NestViewModelFactory — pass OkHttpNestsClient's httpClient via named parameter so the (String) -> OkHttpClient function reference doesn't get bound to the Long callTimeoutMs slot. Fixes the pre-existing :amethyst:compilePlayDebugKotlin failure on the branch base. Adds three strings (loading, unjoinable title/body, back). https://claude.ai/code/session_01DMeCvWyBYVVVPez2hwqCs4
This commit is contained in:
+19
-17
@@ -331,23 +331,25 @@ private fun ScheduleStartPicker(
|
|||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
val dayMillisUtc = datePickerState.selectedDateMillis
|
val dayMillisUtc = datePickerState.selectedDateMillis
|
||||||
if (dayMillisUtc != null) {
|
if (dayMillisUtc != null) {
|
||||||
// DatePicker hands back UTC midnight of the selected
|
// DatePicker hands back UTC midnight of the
|
||||||
// calendar date. Add the picked hour:minute (local)
|
// selected calendar date. Reinterpret that
|
||||||
// to its seconds, then subtract the local offset to
|
// calendar date as local + the picked
|
||||||
// land on the correct UTC unix second — same shape
|
// hour:minute, then convert to UTC seconds
|
||||||
// as ExpirationDatePicker.
|
// using the zone offset AT THE PICKED INSTANT.
|
||||||
val localSeconds =
|
// The previous code used `Instant.now()`'s
|
||||||
dayMillisUtc / 1000L +
|
// offset, which was off by one hour for rooms
|
||||||
timePickerState.hour * 3600L +
|
// scheduled across a DST transition.
|
||||||
timePickerState.minute * 60L
|
val zone = java.time.ZoneId.systemDefault()
|
||||||
val offsetSec =
|
val localDate =
|
||||||
java.time.ZoneId
|
java.time.Instant
|
||||||
.systemDefault()
|
.ofEpochMilli(dayMillisUtc)
|
||||||
.rules
|
.atZone(java.time.ZoneOffset.UTC)
|
||||||
.getOffset(java.time.Instant.now())
|
.toLocalDate()
|
||||||
.totalSeconds
|
val pickedZdt =
|
||||||
.toLong()
|
localDate
|
||||||
onChange(localSeconds - offsetSec)
|
.atTime(timePickerState.hour, timePickerState.minute)
|
||||||
|
.atZone(zone)
|
||||||
|
onChange(pickedZdt.toEpochSecond())
|
||||||
}
|
}
|
||||||
showTime = false
|
showTime = false
|
||||||
}) { Text(stringRes(R.string.nest_create_submit)) }
|
}) { Text(stringRes(R.string.nest_create_submit)) }
|
||||||
|
|||||||
+119
-32
@@ -21,6 +21,15 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -28,6 +37,11 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.commons.viewmodels.ConnectionUiState
|
import com.vitorpamplona.amethyst.commons.viewmodels.ConnectionUiState
|
||||||
import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel
|
import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel
|
||||||
@@ -46,6 +60,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.lifecycle.PipBri
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.lifecycle.rememberNestViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.lifecycle.rememberNestViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.screen.NestFullScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.screen.NestFullScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.screen.NestPipScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.screen.NestPipScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.nestsclient.NestsRoomConfig
|
import com.vitorpamplona.nestsclient.NestsRoomConfig
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
||||||
@@ -71,45 +86,117 @@ internal fun NestActivityContent(
|
|||||||
) {
|
) {
|
||||||
val parsedAddress = remember(addressValue) { Address.parse(addressValue) }
|
val parsedAddress = remember(addressValue) { Address.parse(addressValue) }
|
||||||
if (parsedAddress == null) {
|
if (parsedAddress == null) {
|
||||||
// Malformed address — bail. The Activity will receive a Closed
|
// Malformed address — show the unjoinable state so the user
|
||||||
// signal through the VM teardown when the user finishes; until
|
// gets a clear path back instead of staring at a black screen
|
||||||
// then we just render nothing.
|
// while the Activity sits behind a useless extra.
|
||||||
|
UnjoinableRoomState(onLeave = onLeave)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadAddressableNote(parsedAddress, accountViewModel) { addressableNote ->
|
LoadAddressableNote(parsedAddress, accountViewModel) { addressableNote ->
|
||||||
addressableNote ?: return@LoadAddressableNote
|
if (addressableNote == null) {
|
||||||
|
// Note hasn't resolved yet — relay subscription is in
|
||||||
|
// flight. Render a centered spinner so the user sees that
|
||||||
|
// we're working on it (audit Android #6).
|
||||||
|
LoadingRoomState()
|
||||||
|
return@LoadAddressableNote
|
||||||
|
}
|
||||||
val event by observeNoteEvent<MeetingSpaceEvent>(addressableNote, accountViewModel)
|
val event by observeNoteEvent<MeetingSpaceEvent>(addressableNote, accountViewModel)
|
||||||
|
|
||||||
event?.let {
|
val current = event
|
||||||
val service = it.service()
|
if (current == null) {
|
||||||
val endPoint = it.endpoint()
|
LoadingRoomState()
|
||||||
if (service != null && endPoint != null) {
|
return@LoadAddressableNote
|
||||||
val viewModel =
|
}
|
||||||
rememberNestViewModel(
|
val service = current.service()
|
||||||
NestsRoomConfig(
|
val endPoint = current.endpoint()
|
||||||
authBaseUrl = service,
|
if (service.isNullOrBlank() || endPoint.isNullOrBlank()) {
|
||||||
endpoint = endPoint,
|
UnjoinableRoomState(onLeave = onLeave)
|
||||||
hostPubkey = it.pubKey,
|
return@LoadAddressableNote
|
||||||
roomId = it.dTag(),
|
}
|
||||||
kind = it.kind,
|
val viewModel =
|
||||||
),
|
rememberNestViewModel(
|
||||||
accountViewModel.account.signer,
|
NestsRoomConfig(
|
||||||
)
|
authBaseUrl = service,
|
||||||
|
endpoint = endPoint,
|
||||||
|
hostPubkey = current.pubKey,
|
||||||
|
roomId = current.dTag(),
|
||||||
|
kind = current.kind,
|
||||||
|
),
|
||||||
|
accountViewModel.account.signer,
|
||||||
|
)
|
||||||
|
|
||||||
NestActivityBody(
|
NestActivityBody(
|
||||||
event = it,
|
event = current,
|
||||||
roomNote = addressableNote,
|
roomNote = addressableNote,
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
isInPipMode = isInPipMode,
|
isInPipMode = isInPipMode,
|
||||||
isPipSupported = isPipSupported,
|
isPipSupported = isPipSupported,
|
||||||
onMuteState = onMuteState,
|
onMuteState = onMuteState,
|
||||||
onConnectedChange = onConnectedChange,
|
onConnectedChange = onConnectedChange,
|
||||||
pipMuteSignal = pipMuteSignal,
|
pipMuteSignal = pipMuteSignal,
|
||||||
onLeave = onLeave,
|
onLeave = onLeave,
|
||||||
onMinimize = onMinimize,
|
onMinimize = onMinimize,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centered spinner shown between activity launch and the first
|
||||||
|
* resolved kind-30312 event. Replaces the previous black screen so
|
||||||
|
* the user knows the room is being loaded, not stuck.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun LoadingRoomState() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.nest_loading_room),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Terminal state for rooms whose kind-30312 doesn't carry the
|
||||||
|
* service/endpoint pair the audio plane needs (or whose address is
|
||||||
|
* malformed). Renders a clear explanation + a Back button so the
|
||||||
|
* user can leave instead of force-killing the activity.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun UnjoinableRoomState(onLeave: () -> Unit) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(24.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.nest_unjoinable_title),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.nest_unjoinable_body),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
OutlinedButton(onClick = onLeave) {
|
||||||
|
Text(stringRes(R.string.nest_unjoinable_back))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -47,7 +47,11 @@ internal class NestViewModelFactory(
|
|||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
||||||
NestViewModel(
|
NestViewModel(
|
||||||
httpClient = OkHttpNestsClient(Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForVideo),
|
// Named parameter — `OkHttpNestsClient`'s primary constructor
|
||||||
|
// declares `callTimeoutMs` first with a default, so the
|
||||||
|
// function reference must be passed explicitly to the
|
||||||
|
// `httpClient` slot rather than as the first positional arg.
|
||||||
|
httpClient = OkHttpNestsClient(httpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForVideo),
|
||||||
transport = QuicWebTransportFactory(),
|
transport = QuicWebTransportFactory(),
|
||||||
decoderFactory = { MediaCodecOpusDecoder() },
|
decoderFactory = { MediaCodecOpusDecoder() },
|
||||||
playerFactory = { AudioTrackPlayer() },
|
playerFactory = { AudioTrackPlayer() },
|
||||||
|
|||||||
+4
@@ -242,6 +242,10 @@ private fun OnStageControls(
|
|||||||
|
|
||||||
is BroadcastUiState.Broadcasting -> {
|
is BroadcastUiState.Broadcasting -> {
|
||||||
MicMuteToggle(isMuted = broadcast.isMuted, onToggle = viewModel::setMicMuted)
|
MicMuteToggle(isMuted = broadcast.isMuted, onToggle = viewModel::setMicMuted)
|
||||||
|
// Stop sending audio without leaving the stage — viewer
|
||||||
|
// can pause the mic and resume later via Talk. Distinct
|
||||||
|
// from [LeaveStageButton], which also vacates the slot.
|
||||||
|
StopBroadcastButton(onClick = viewModel::stopBroadcast)
|
||||||
LeaveStageButton(onClick = leaveStage)
|
LeaveStageButton(onClick = leaveStage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+62
-30
@@ -24,22 +24,29 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
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.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel
|
import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel
|
||||||
|
import com.vitorpamplona.amethyst.commons.viewmodels.RoomPresence
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.participants.RoomParticipantActions
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.participants.RoomParticipantActions
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
@@ -68,54 +75,79 @@ internal fun HandRaiseQueueSection(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val presences by viewModel.presences.collectAsState()
|
val presences by viewModel.presences.collectAsState()
|
||||||
|
// Memoize the on-stage gate so a heartbeat that doesn't change
|
||||||
|
// the kind-30312 doesn't re-build the set every recompose. The
|
||||||
|
// event reference is stable across recompositions until the host
|
||||||
|
// republishes; presence updates run through the second remember.
|
||||||
val onStageKeys =
|
val onStageKeys =
|
||||||
event
|
remember(event) {
|
||||||
.participants()
|
event
|
||||||
.filter { it.canSpeak() }
|
.participants()
|
||||||
.map { it.pubKey }
|
.filter { it.canSpeak() }
|
||||||
.toSet()
|
.map { it.pubKey }
|
||||||
|
.toSet()
|
||||||
|
}
|
||||||
val hands =
|
val hands =
|
||||||
presences.values
|
remember(presences, onStageKeys) {
|
||||||
.filter { it.handRaised && it.pubkey !in onStageKeys }
|
presences.values
|
||||||
.sortedBy { it.updatedAtSec }
|
.filter { it.handRaised && it.pubkey !in onStageKeys }
|
||||||
|
.sortedBy { it.updatedAtSec }
|
||||||
|
}
|
||||||
|
|
||||||
if (hands.isEmpty()) return
|
if (hands.isEmpty()) return
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
Column(modifier = modifier.fillMaxWidth().padding(top = 12.dp)) {
|
Column(modifier = modifier.fillMaxSize().padding(top = 12.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = stringRes(R.string.nest_hand_raise_queue_title),
|
text = stringRes(R.string.nest_hand_raise_queue_title),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
hands.forEach { hand ->
|
// LazyColumn so a flood of raised hands doesn't render every
|
||||||
Row(
|
// row every recompose. Stable key by pubkey lets Compose
|
||||||
modifier = Modifier.fillMaxWidth().padding(top = 6.dp),
|
// animate row entry/exit instead of teardown-rebuild.
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
LazyColumn(modifier = Modifier.fillMaxSize().padding(top = 6.dp)) {
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
items(items = hands, key = { it.pubkey }) { hand ->
|
||||||
) {
|
HandRaiseRow(
|
||||||
ClickableUserPicture(
|
hand = hand,
|
||||||
baseUserHex = hand.pubkey,
|
|
||||||
size = Size35dp,
|
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
)
|
onApprove = {
|
||||||
Text(
|
|
||||||
text = hand.pubkey.take(8),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(4.dp))
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val template = RoomParticipantActions.setRole(event, hand.pubkey, ROLE.SPEAKER)
|
val template = RoomParticipantActions.setRole(event, hand.pubkey, ROLE.SPEAKER)
|
||||||
template?.let { runCatching { accountViewModel.account.signAndComputeBroadcast(it) } }
|
template?.let { runCatching { accountViewModel.account.signAndComputeBroadcast(it) } }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
)
|
||||||
Text(stringRes(R.string.nest_hand_raise_approve))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HandRaiseRow(
|
||||||
|
hand: RoomPresence,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
onApprove: () -> Unit,
|
||||||
|
) {
|
||||||
|
val user = remember(hand.pubkey) { LocalCache.getOrCreateUser(hand.pubkey) }
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(top = 6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
ClickableUserPicture(
|
||||||
|
baseUserHex = hand.pubkey,
|
||||||
|
size = Size35dp,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
UsernameDisplay(
|
||||||
|
baseUser = user,
|
||||||
|
weight = Modifier.weight(1f),
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(4.dp))
|
||||||
|
Button(onClick = onApprove) {
|
||||||
|
Text(stringRes(R.string.nest_hand_raise_approve))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -591,6 +591,10 @@
|
|||||||
<string name="nest_minimize">Minimize</string>
|
<string name="nest_minimize">Minimize</string>
|
||||||
<string name="nest_minimize_description">Minimize to keep listening</string>
|
<string name="nest_minimize_description">Minimize to keep listening</string>
|
||||||
<string name="nest_audio_dropped">Audio dropped</string>
|
<string name="nest_audio_dropped">Audio dropped</string>
|
||||||
|
<string name="nest_loading_room">Loading room…</string>
|
||||||
|
<string name="nest_unjoinable_title">This room can\'t be opened</string>
|
||||||
|
<string name="nest_unjoinable_body">The room is missing an audio service or endpoint. The host needs to update its details before listeners can connect.</string>
|
||||||
|
<string name="nest_unjoinable_back">Back</string>
|
||||||
<string name="nest_create_fab">Start space</string>
|
<string name="nest_create_fab">Start space</string>
|
||||||
<string name="nest_no_server_title">Set up a nest server</string>
|
<string name="nest_no_server_title">Set up a nest server</string>
|
||||||
<string name="nest_no_server_body">You haven\'t picked a nest server yet. Add %1$s to your server list and continue?\n\nYou can change this later in Settings.</string>
|
<string name="nest_no_server_body">You haven\'t picked a nest server yet. Add %1$s to your server list and continue?\n\nYou can change this later in Settings.</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user