mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
feat(privacylock): redesign Set/Change password dialog + snackbar
Rewrites SetPasswordDialog.kt with modern 2024-2026 UX. Adds
"Privacy lock enabled" / "Password updated" confirmation snackbars.
Dialog changes:
- Dialog + Surface(shape=shapes.large, tonal=6.dp) shell instead of
default AlertDialog. Fixed width 440dp. Matches NewDmDialog.kt.
- Header row with Lock icon + title (titleLarge). Softer body copy
under the header for the first-time-set path.
- Set-a-password flow uses ONE password field with a reveal toggle
(Visibility / VisibilityOff, per-field independent). The reveal
toggle IS the confirmation — no more "confirm password" field.
Matches WhatsApp Chat Lock + macOS Users & Groups.
- Change-password flow uses two fields (current + new), each with
its own reveal toggle. Current is verification, not redundancy.
- Real-time checklist row under the New field: green CheckCircle +
"Min 6 characters" when satisfied, outlined Circle + muted text
otherwise. Copy-pattern from EditProfileScreen NIP-05 status.
- Save button disabled until the checklist passes.
- Bumps PRIVACY_LOCK_MIN_PASSWORD_LENGTH from 4 to 6.
- Auto-focus first field on open (LaunchedEffect + FocusRequester).
- Enter submits (via onPreviewKeyEvent + KeyboardActions.onDone).
- Escape / click-outside-dismiss are disabled to prevent accidental
loss of typed password (dismissOnClickOutside = false).
- Wrong-current error shown inline under the Current field.
- All reveal toggles reuse the KeyInputField.kt idiom verbatim.
Snackbar plumbing (scope-local — no CompositionLocal):
- PrivacyLockSettingsScreen owns a SnackbarHostState overlaid at
Alignment.BottomCenter. LockToggleCard receives an onSaved
callback, fires "Privacy lock enabled" on first-time set or
"Password updated" on change.
- DesktopMessagesScreen (banner path) owns its own SnackbarHostState
overlaid at BottomCenter. MessagesFirstRunBanner takes an
optional onSaved callback (default {}), fires "Privacy lock
enabled" after the dialog saves.
This commit is contained in:
+2
-1
@@ -62,7 +62,7 @@ import com.vitorpamplona.amethyst.commons.privacylock.LocalMessagesLockState
|
|||||||
* content, so this composable never composes in that case).
|
* content, so this composable never composes in that case).
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun MessagesFirstRunBanner() {
|
fun MessagesFirstRunBanner(onSaved: (String) -> Unit = {}) {
|
||||||
val settings = LocalPrivacyLockSettings.current
|
val settings = LocalPrivacyLockSettings.current
|
||||||
val lockState = LocalMessagesLockState.current
|
val lockState = LocalMessagesLockState.current
|
||||||
val enabled by settings.lockEnabled.collectAsState()
|
val enabled by settings.lockEnabled.collectAsState()
|
||||||
@@ -123,6 +123,7 @@ fun MessagesFirstRunBanner() {
|
|||||||
// right after they just entered the password.
|
// right after they just entered the password.
|
||||||
lockState.onUnlockSuccess()
|
lockState.onUnlockSuccess()
|
||||||
showDialog = false
|
showDialog = false
|
||||||
|
onSaved("Privacy lock enabled")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+220
-51
@@ -22,29 +22,65 @@ package com.vitorpamplona.amethyst.desktop.security
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.input.key.Key
|
||||||
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
|
import androidx.compose.ui.input.key.key
|
||||||
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
|
import androidx.compose.ui.input.key.type
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||||
|
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||||
|
|
||||||
|
/** Enforced minimum length for a new/rotated password. */
|
||||||
|
const val PRIVACY_LOCK_MIN_PASSWORD_LENGTH = 6
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set or change the privacy-lock password.
|
* Set or change the privacy-lock password.
|
||||||
*
|
*
|
||||||
* Pass [existingHash] = null when the user hasn't set a password yet
|
* Pass [existingHash] = null when the user hasn't set a password yet
|
||||||
* (first-run banner path, or a fresh Settings toggle). In that case the
|
* (first-run banner path, or a fresh Settings toggle). In that case the
|
||||||
* "current password" field is hidden. Pass a real hash to force
|
* dialog shows a single New password field with a reveal toggle. Pass a
|
||||||
* verification of the current password before letting the user rotate.
|
* real hash to force verification of the current password before letting
|
||||||
|
* the user rotate — the dialog then shows a Current password field
|
||||||
|
* followed by the New password field, each with its own reveal toggle.
|
||||||
*
|
*
|
||||||
* On successful validation, [onConfirm] is invoked with a fresh
|
* On successful validation, [onConfirm] is invoked with a fresh
|
||||||
* `salt$hash` string ready for [com.vitorpamplona.amethyst.commons.privacylock.PrivacyLockSettings.setPasswordHashed].
|
* `salt$hash` string ready for [com.vitorpamplona.amethyst.commons.privacylock.PrivacyLockSettings.setPasswordHashed].
|
||||||
|
*
|
||||||
|
* UX: auto-focus the first field on open, Enter submits, Escape cancels.
|
||||||
|
* Real-time checklist under the New password field shows a green ✓ once
|
||||||
|
* the length threshold is reached. Reveal toggles are per-field
|
||||||
|
* independent so revealing Current does not reveal New.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun SetPasswordDialog(
|
fun SetPasswordDialog(
|
||||||
@@ -52,74 +88,207 @@ fun SetPasswordDialog(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onConfirm: (String) -> Unit,
|
onConfirm: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val isChange = existingHash != null
|
||||||
var current by remember { mutableStateOf("") }
|
var current by remember { mutableStateOf("") }
|
||||||
var new1 by remember { mutableStateOf("") }
|
var new by remember { mutableStateOf("") }
|
||||||
var new2 by remember { mutableStateOf("") }
|
var currentError by remember { mutableStateOf<String?>(null) }
|
||||||
var error by remember { mutableStateOf<String?>(null) }
|
var newError by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
val firstFieldFocus = remember { FocusRequester() }
|
||||||
|
|
||||||
val submit: () -> Unit = {
|
val submit: () -> Unit = {
|
||||||
val currentOk =
|
val currentOk =
|
||||||
existingHash == null ||
|
!isChange ||
|
||||||
PasswordHasher.verify(current.toCharArray(), existingHash)
|
PasswordHasher.verify(current.toCharArray(), existingHash!!)
|
||||||
when {
|
when {
|
||||||
!currentOk -> error = "Current password is wrong"
|
!currentOk -> {
|
||||||
new1.length < 4 -> error = "New password must be at least 4 characters"
|
currentError = "Wrong password"
|
||||||
new1 != new2 -> error = "Passwords don't match"
|
newError = null
|
||||||
else -> onConfirm(PasswordHasher.hash(new1.toCharArray()))
|
}
|
||||||
|
new.length < PRIVACY_LOCK_MIN_PASSWORD_LENGTH -> {
|
||||||
|
currentError = null
|
||||||
|
newError = "Must be at least $PRIVACY_LOCK_MIN_PASSWORD_LENGTH characters"
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
onConfirm(PasswordHasher.hash(new.toCharArray()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AlertDialog(
|
LaunchedEffect(Unit) {
|
||||||
|
firstFieldFocus.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text(if (existingHash == null) "Set a password" else "Change password") },
|
properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = false),
|
||||||
text = {
|
) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
Surface(
|
||||||
if (existingHash != null) {
|
shape = MaterialTheme.shapes.large,
|
||||||
OutlinedTextField(
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
tonalElevation = 6.dp,
|
||||||
|
modifier = Modifier.width(440.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(24.dp).fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
DialogHeader(isChange = isChange)
|
||||||
|
|
||||||
|
if (!isChange) {
|
||||||
|
Text(
|
||||||
|
text = "Choose a password to lock the Messages tab. You'll enter it to unlock later.",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isChange) {
|
||||||
|
PasswordField(
|
||||||
value = current,
|
value = current,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
current = it
|
current = it
|
||||||
error = null
|
currentError = null
|
||||||
},
|
},
|
||||||
label = { Text("Current password") },
|
label = "Current password",
|
||||||
singleLine = true,
|
errorMessage = currentError,
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
modifier = Modifier.focusRequester(firstFieldFocus),
|
||||||
|
imeAction = ImeAction.Next,
|
||||||
|
onImeAction = { /* Tab handled by focus system */ },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
OutlinedTextField(
|
|
||||||
value = new1,
|
PasswordField(
|
||||||
|
value = new,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
new1 = it
|
new = it
|
||||||
error = null
|
newError = null
|
||||||
},
|
},
|
||||||
label = { Text("New password") },
|
label = "New password",
|
||||||
singleLine = true,
|
errorMessage = newError,
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
modifier =
|
||||||
|
if (isChange) Modifier else Modifier.focusRequester(firstFieldFocus),
|
||||||
|
imeAction = ImeAction.Done,
|
||||||
|
onImeAction = { submit() },
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
|
||||||
value = new2,
|
RequirementChecklistRow(satisfied = new.length >= PRIVACY_LOCK_MIN_PASSWORD_LENGTH)
|
||||||
onValueChange = {
|
|
||||||
new2 = it
|
Row(
|
||||||
error = null
|
modifier = Modifier.fillMaxWidth(),
|
||||||
},
|
horizontalArrangement = Arrangement.End,
|
||||||
label = { Text("Confirm new password") },
|
) {
|
||||||
singleLine = true,
|
TextButton(onClick = onDismiss) { Text("Cancel") }
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
Button(
|
||||||
)
|
onClick = submit,
|
||||||
error?.let {
|
enabled = new.length >= PRIVACY_LOCK_MIN_PASSWORD_LENGTH,
|
||||||
Text(
|
) {
|
||||||
text = it,
|
Text("Save")
|
||||||
color = MaterialTheme.colorScheme.error,
|
}
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DialogHeader(isChange: Boolean) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
symbol = MaterialSymbols.Lock,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = if (isChange) "Change password" else "Set a password",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PasswordField(
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
label: String,
|
||||||
|
errorMessage: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
imeAction: ImeAction = ImeAction.Done,
|
||||||
|
onImeAction: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
var revealed by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
label = { Text(label) },
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.onPreviewKeyEvent { event ->
|
||||||
|
if (event.type == KeyEventType.KeyDown && event.key == Key.Enter && imeAction == ImeAction.Done) {
|
||||||
|
onImeAction()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
visualTransformation = if (revealed) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Password,
|
||||||
|
imeAction = imeAction,
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(onDone = { onImeAction() }, onNext = { onImeAction() }),
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { revealed = !revealed }) {
|
||||||
|
Icon(
|
||||||
|
symbol = if (revealed) MaterialSymbols.VisibilityOff else MaterialSymbols.Visibility,
|
||||||
|
contentDescription = if (revealed) "Hide password" else "Show password",
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
isError = errorMessage != null,
|
||||||
TextButton(onClick = submit) { Text("Save") }
|
supportingText =
|
||||||
},
|
errorMessage?.let {
|
||||||
dismissButton = {
|
{
|
||||||
TextButton(onClick = onDismiss) { Text("Cancel") }
|
Text(it, color = MaterialTheme.colorScheme.error)
|
||||||
},
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RequirementChecklistRow(satisfied: Boolean) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
symbol = if (satisfied) MaterialSymbols.CheckCircle else MaterialSymbols.Circle,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint =
|
||||||
|
if (satisfied) {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Min $PRIVACY_LOCK_MIN_PASSWORD_LENGTH characters",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color =
|
||||||
|
if (satisfied) {
|
||||||
|
MaterialTheme.colorScheme.onSurface
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+44
-29
@@ -29,9 +29,12 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
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
|
||||||
@@ -67,6 +70,7 @@ import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
|||||||
import com.vitorpamplona.amethyst.desktop.security.MessagesFirstRunBanner
|
import com.vitorpamplona.amethyst.desktop.security.MessagesFirstRunBanner
|
||||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.awt.Cursor
|
import java.awt.Cursor
|
||||||
|
|
||||||
private val isMacOS = System.getProperty("os.name").lowercase().contains("mac")
|
private val isMacOS = System.getProperty("os.name").lowercase().contains("mac")
|
||||||
@@ -128,37 +132,48 @@ fun DesktopMessagesScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
MessagesFirstRunBanner()
|
val snackbarScope = rememberCoroutineScope()
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
|
||||||
if (compactMode) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
CompactMessagesContent(
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
selectedRoom = selectedRoom,
|
MessagesFirstRunBanner(onSaved = { msg ->
|
||||||
listState = listState,
|
snackbarScope.launch { snackbarHostState.showSnackbar(msg) }
|
||||||
account = account,
|
})
|
||||||
cacheProvider = cacheProvider,
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
scope = scope,
|
if (compactMode) {
|
||||||
onNavigateToProfile = onNavigateToProfile,
|
CompactMessagesContent(
|
||||||
listFocusRequester = listFocusRequester,
|
selectedRoom = selectedRoom,
|
||||||
onShowNewDm = { showNewDmDialog = true },
|
listState = listState,
|
||||||
onShowRelayPicker = { showDmRelayPicker = true },
|
account = account,
|
||||||
keyHandler = keyHandler,
|
cacheProvider = cacheProvider,
|
||||||
)
|
scope = scope,
|
||||||
} else {
|
onNavigateToProfile = onNavigateToProfile,
|
||||||
SplitMessagesContent(
|
listFocusRequester = listFocusRequester,
|
||||||
selectedRoom = selectedRoom,
|
onShowNewDm = { showNewDmDialog = true },
|
||||||
listState = listState,
|
onShowRelayPicker = { showDmRelayPicker = true },
|
||||||
account = account,
|
keyHandler = keyHandler,
|
||||||
cacheProvider = cacheProvider,
|
)
|
||||||
scope = scope,
|
} else {
|
||||||
onNavigateToProfile = onNavigateToProfile,
|
SplitMessagesContent(
|
||||||
listFocusRequester = listFocusRequester,
|
selectedRoom = selectedRoom,
|
||||||
onShowNewDm = { showNewDmDialog = true },
|
listState = listState,
|
||||||
onShowRelayPicker = { showDmRelayPicker = true },
|
account = account,
|
||||||
keyHandler = keyHandler,
|
cacheProvider = cacheProvider,
|
||||||
)
|
scope = scope,
|
||||||
|
onNavigateToProfile = onNavigateToProfile,
|
||||||
|
listFocusRequester = listFocusRequester,
|
||||||
|
onShowNewDm = { showNewDmDialog = true },
|
||||||
|
onShowRelayPicker = { showDmRelayPicker = true },
|
||||||
|
keyHandler = keyHandler,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SnackbarHost(
|
||||||
|
hostState = snackbarHostState,
|
||||||
|
modifier = Modifier.align(Alignment.BottomCenter).padding(16.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showNewDmDialog) {
|
if (showNewDmDialog) {
|
||||||
|
|||||||
+36
-12
@@ -21,6 +21,7 @@
|
|||||||
package com.vitorpamplona.amethyst.desktop.ui.settings
|
package com.vitorpamplona.amethyst.desktop.ui.settings
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
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.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -33,6 +34,8 @@ import androidx.compose.material3.DropdownMenu
|
|||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -40,6 +43,7 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -49,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.privacylock.InactivityTimer
|
|||||||
import com.vitorpamplona.amethyst.commons.privacylock.PrivacyLockSettings
|
import com.vitorpamplona.amethyst.commons.privacylock.PrivacyLockSettings
|
||||||
import com.vitorpamplona.amethyst.desktop.security.LocalPrivacyLockSettings
|
import com.vitorpamplona.amethyst.desktop.security.LocalPrivacyLockSettings
|
||||||
import com.vitorpamplona.amethyst.desktop.security.SetPasswordDialog
|
import com.vitorpamplona.amethyst.desktop.security.SetPasswordDialog
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Desktop privacy-lock settings pane. Column + Card layout (no Scaffold) —
|
* Desktop privacy-lock settings pane. Column + Card layout (no Scaffold) —
|
||||||
@@ -57,22 +62,39 @@ import com.vitorpamplona.amethyst.desktop.security.SetPasswordDialog
|
|||||||
@Composable
|
@Composable
|
||||||
fun PrivacyLockSettingsScreen() {
|
fun PrivacyLockSettingsScreen() {
|
||||||
val settings = LocalPrivacyLockSettings.current
|
val settings = LocalPrivacyLockSettings.current
|
||||||
Column(
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
modifier =
|
val scope = rememberCoroutineScope()
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
.padding(16.dp),
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
modifier =
|
||||||
) {
|
Modifier
|
||||||
LockToggleCard(settings)
|
.fillMaxWidth()
|
||||||
InactivityCard(settings)
|
.padding(16.dp),
|
||||||
RedactionCard(settings)
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
LimitationsCard()
|
) {
|
||||||
|
LockToggleCard(
|
||||||
|
settings = settings,
|
||||||
|
onSaved = { message ->
|
||||||
|
scope.launch { snackbarHostState.showSnackbar(message) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
InactivityCard(settings)
|
||||||
|
RedactionCard(settings)
|
||||||
|
LimitationsCard()
|
||||||
|
}
|
||||||
|
SnackbarHost(
|
||||||
|
hostState = snackbarHostState,
|
||||||
|
modifier = Modifier.align(Alignment.BottomCenter).padding(16.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LockToggleCard(settings: PrivacyLockSettings) {
|
private fun LockToggleCard(
|
||||||
|
settings: PrivacyLockSettings,
|
||||||
|
onSaved: (String) -> Unit,
|
||||||
|
) {
|
||||||
val enabled by settings.lockEnabled.collectAsState()
|
val enabled by settings.lockEnabled.collectAsState()
|
||||||
val stored by settings.passwordHashed.collectAsState()
|
val stored by settings.passwordHashed.collectAsState()
|
||||||
var showSetPassword by remember { mutableStateOf(false) }
|
var showSetPassword by remember { mutableStateOf(false) }
|
||||||
@@ -117,6 +139,7 @@ private fun LockToggleCard(settings: PrivacyLockSettings) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showSetPassword) {
|
if (showSetPassword) {
|
||||||
|
val wasFirstSet = stored == null
|
||||||
SetPasswordDialog(
|
SetPasswordDialog(
|
||||||
existingHash = stored,
|
existingHash = stored,
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
@@ -128,6 +151,7 @@ private fun LockToggleCard(settings: PrivacyLockSettings) {
|
|||||||
if (pendingEnable) settings.setLockEnabled(true)
|
if (pendingEnable) settings.setLockEnabled(true)
|
||||||
showSetPassword = false
|
showSetPassword = false
|
||||||
pendingEnable = false
|
pendingEnable = false
|
||||||
|
onSaved(if (wasFirstSet) "Privacy lock enabled" else "Password updated")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user