Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/src/main/kotlin/com/privee/app/data/AppContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,17 @@ class AppContainer(private val context: Context) {
runCatching { session.server.api.registerPush(session.account.token, endpoint) }
}

/** Signs out; the keys stay on the device for the next sign-in on the same server. */
/**
* Signs out; the keys stay on the device for the next sign-in on the same server,
* but the local hints about peers are removed.
*/
suspend fun signOut(remote: Boolean = true) {
val session = _session.value ?: return
backgroundListening.disable()
BackgroundMessageService.stop(context)
_session.value = null
session.stop()
runCatching { session.signal.clearPeerHints() }
PushRegistration.unregister(context)
val api = session.server.api
if (remote) {
Expand Down
39 changes: 39 additions & 0 deletions app/src/main/kotlin/com/privee/app/ui/ChatScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ fun ChatScreen(container: AppContainer, session: PriveeSession, peerName: String
val deviceState by session.deviceState.collectAsStateWithLifecycle()
val connected by session.connected.collectAsStateWithLifecycle()
var confirmClear by rememberSaveable { mutableStateOf(false) }
var editHint by rememberSaveable { mutableStateOf(false) }
val signalState by session.signal.changes.collectAsStateWithLifecycle()
val hint = peerId?.let { signalState.peers[it.toString()]?.hint }

DisposableEffect(peerName) {
container.activeChat.value = peerName
Expand All @@ -154,8 +157,23 @@ fun ChatScreen(container: AppContainer, session: PriveeSession, peerName: String
onApprove = { vm.approve() },
onDismissNotice = c::dismissNotice,
onResetIdentity = { vm.viewModelScope.launch(Dispatchers.IO) { runCatching { session.resetIdentity() } } },
hint = hint,
onEditHint = { editHint = true },
)

val hintPeer = peerId
if (editHint && hintPeer != null) {
HintDialog(
peerName = peerName,
current = hint,
onSave = { value ->
editHint = false
vm.viewModelScope.launch(Dispatchers.IO) { runCatching { session.signal.setPeerHint(hintPeer, value) } }
},
onDismiss = { editHint = false },
)
}

if (vm.safetyNumberVisible) SafetyNumberDialog(peerName, vm.safetyNumber, vm::hideSafetyNumber)

if (confirmClear) {
Expand Down Expand Up @@ -196,6 +214,8 @@ fun ChatContent(
onApprove: () -> Unit,
onDismissNotice: () -> Unit,
onResetIdentity: () -> Unit,
hint: String? = null,
onEditHint: () -> Unit = {},
) {
var menu by rememberSaveable { mutableStateOf(false) }
val listState = rememberLazyListState()
Expand All @@ -213,6 +233,16 @@ fun ChatContent(
Spacer(Modifier.width(12.dp))
Column {
Text(peerName, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.titleMedium)
hint?.let {
Text(
it,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.testTag("chat-hint"),
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
ConnectionDot(connected)
Spacer(Modifier.width(6.dp))
Expand All @@ -235,6 +265,15 @@ fun ChatContent(
}
DropdownMenu(expanded = menu, onDismissRequest = { menu = false }) {
ProtectedWindow()
DropdownMenuItem(
text = { Text(stringResource(if (hint == null) R.string.hint_add else R.string.hint_edit)) },
onClick = {
menu = false
onEditHint()
},
enabled = hasPeer,
modifier = Modifier.testTag("edit-hint"),
)
DropdownMenuItem(
text = { Text(stringResource(R.string.safety_number)) },
onClick = {
Expand Down
91 changes: 91 additions & 0 deletions app/src/main/kotlin/com/privee/app/ui/HintDialog.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.privee.app.ui

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.text.KeyboardActions
import com.privee.app.R
import com.privee.signal.SignalClient

/**
* Edits the local hint about who [peerName] is. The advice not to write their name
* is shown every time, because anyone who unlocks the device can read hints.
*/
@Composable
fun HintDialog(peerName: String, current: String?, onSave: (String?) -> Unit, onDismiss: () -> Unit) {
var text by rememberSaveable(peerName) { mutableStateOf(current.orEmpty()) }
PriveeAlertDialog(
onDismissRequest = onDismiss,
modifier = Modifier.testTag("hint-dialog"),
title = { Text(stringResource(R.string.hint_title, peerName)) },
text = {
Column {
Text(
stringResource(R.string.hint_advice),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.testTag("hint-advice"),
)
Spacer(Modifier.height(12.dp))
NoPersonalizedLearning {
OutlinedTextField(
value = text,
onValueChange = { value ->
text = value.replace('\n', ' ').let {
if (it.codePointCount(0, it.length) > SignalClient.MAX_HINT_LENGTH) {
it.substring(0, it.offsetByCodePoints(0, SignalClient.MAX_HINT_LENGTH))
} else {
it
}
}
},
label = { Text(stringResource(R.string.hint_label)) },
placeholder = { Text(stringResource(R.string.hint_placeholder)) },
supportingText = {
Text("${text.codePointCount(0, text.length)}/${SignalClient.MAX_HINT_LENGTH}")
},
singleLine = true,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Sentences,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(onDone = { onSave(SignalClient.normalizeHint(text)) }),
modifier = Modifier.fillMaxWidth().testTag("hint-input"),
)
}
}
},
confirmButton = {
TextButton(
onClick = { onSave(SignalClient.normalizeHint(text)) },
modifier = Modifier.testTag("hint-save"),
) { Text(stringResource(R.string.save)) }
},
dismissButton = {
if (current != null) {
TextButton(onClick = { onSave(null) }, modifier = Modifier.testTag("hint-remove")) {
Text(stringResource(R.string.remove))
}
} else {
TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) }
}
},
)
}
64 changes: 55 additions & 9 deletions app/src/main/kotlin/com/privee/app/ui/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.privee.app.ui
import android.content.Intent
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
Expand Down Expand Up @@ -66,15 +66,24 @@ import com.privee.signal.SignalState
import kotlinx.coroutines.launch

/** A conversation of the local history, for the home screen. */
data class Recent(val peerName: String, val preview: String?, val ts: Long, val outgoing: Boolean = false)
data class Recent(
val peerName: String,
val preview: String?,
val ts: Long,
val outgoing: Boolean = false,
val peerId: Long? = null,
/** The user's local note about who the peer is. */
val hint: String? = null,
)

fun recents(state: SignalState): List<Recent> {
val last = state.history.values.groupBy { it.peerId }.mapValues { (_, rows) -> rows.maxBy { it.ts } }
return state.peers.mapNotNull { (id, meta) ->
val name = meta.name ?: return@mapNotNull null
val row = last[id.toLongOrNull()]
val peerId = id.toLongOrNull()
val row = last[peerId]
val preview = row?.let { it.plaintext ?: "…" }
Recent(name, preview, row?.ts ?: 0, row?.direction == Direction.Out)
Recent(name, preview, row?.ts ?: 0, row?.direction == Direction.Out, peerId, meta.hint)
}.sortedByDescending { it.ts }
}

Expand All @@ -89,6 +98,7 @@ fun HomeScreen(container: AppContainer, session: PriveeSession, onSettings: () -
var confirmForget by rememberSaveable { mutableStateOf(false) }
var peer by rememberSaveable { mutableStateOf("") }
var peerError by rememberSaveable { mutableStateOf(false) }
var editingHint by rememberSaveable { mutableStateOf<String?>(null) }
val ownName = session.account.session.sessionName
val shareText = stringResource(R.string.share_text, session.server.shareLink(ownName), session.server.appLink(ownName))
val shareTitle = stringResource(R.string.share_session)
Expand All @@ -115,12 +125,13 @@ fun HomeScreen(container: AppContainer, session: PriveeSession, onSettings: () -
}
}

val recents = recents(state)
HomeContent(
ownName = ownName,
serverLabel = session.server.config.label,
connected = connected,
deviceState = deviceState,
recents = recents(state),
recents = recents,
peer = peer,
peerError = peerError,
onPeerChange = {
Expand All @@ -135,8 +146,22 @@ fun HomeScreen(container: AppContainer, session: PriveeSession, onSettings: () -
onForgetDevice = { confirmForget = true },
onResetIdentity = { scope.launch { runCatching { session.resetIdentity() } } },
alerts = { BackgroundAlerts(container, session) },
onEditHint = { editingHint = it.peerName },
)

recents.firstOrNull { it.peerName == editingHint }?.let { recent ->
val peerId = recent.peerId
HintDialog(
peerName = recent.peerName,
current = recent.hint,
onSave = { hint ->
editingHint = null
if (peerId != null) scope.launch { runCatching { session.signal.setPeerHint(peerId, hint) } }
},
onDismiss = { editingHint = null },
)
}

if (confirmForget) {
PriveeAlertDialog(
onDismissRequest = { confirmForget = false },
Expand Down Expand Up @@ -175,6 +200,7 @@ fun HomeContent(
onForgetDevice: () -> Unit,
onResetIdentity: () -> Unit,
alerts: @Composable () -> Unit,
onEditHint: (Recent) -> Unit = {},
) {
var menu by rememberSaveable { mutableStateOf(false) }
Scaffold(
Expand Down Expand Up @@ -287,10 +313,15 @@ fun HomeContent(
if (recents.isNotEmpty()) {
item {
Text(stringResource(R.string.conversations_device), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 8.dp))
Text(
stringResource(R.string.hint_long_press),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
items(recents, key = { it.peerName }) { recent ->
RecentRow(recent) { onOpenChat(recent.peerName) }
RecentRow(recent, onClick = { onOpenChat(recent.peerName) }, onLongClick = { onEditHint(recent) })
}
}
}
Expand Down Expand Up @@ -337,20 +368,35 @@ fun DeviceBanner(state: DeviceState, onReset: () -> Unit) {
}

@Composable
private fun RecentRow(recent: Recent, onClick: () -> Unit) {
private fun RecentRow(recent: Recent, onClick: () -> Unit, onLongClick: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.surfaceContainer)
.clickable(onClick = onClick)
.padding(16.dp),
.combinedClickable(
onClick = onClick,
onLongClick = if (recent.peerId != null) onLongClick else null,
onLongClickLabel = stringResource(if (recent.hint == null) R.string.hint_add else R.string.hint_edit),
)
.padding(16.dp)
.testTag("recent-${recent.peerName}"),
verticalAlignment = Alignment.CenterVertically,
) {
Avatar(recent.peerName)
Spacer(Modifier.width(14.dp))
Column(Modifier.weight(1f)) {
Text(recent.peerName, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis)
recent.hint?.let {
Text(
it,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.testTag("recent-hint"),
)
}
recent.preview?.let {
Text(
if (recent.outgoing) stringResource(R.string.outgoing_preview, it) else it,
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/res/values-b+pt+PT/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,13 @@
<string name="listener_battery_exempt">O Privee está isento da otimização da bateria do Android. Podem continuar a aplicar-se outras restrições do dispositivo.</string>
<string name="listener_settings_unavailable">O Android não conseguiu abrir estas definições. Abra manualmente as definições da aplicação Privee.</string>
<string name="listener_preference_failed">Não foi possível guardar a definição de escuta em segundo plano. Tente novamente.</string>
<string name="hint_add">Adicionar pista</string>
<string name="hint_edit">Editar pista</string>
<string name="hint_title">Pista para %1$s</string>
<string name="hint_advice">Só tu vês esta pista, e apenas neste dispositivo. Não escrevas o nome da pessoa: qualquer pessoa que desbloqueie este dispositivo pode lê-la.</string>
<string name="hint_label">Pista</string>
<string name="hint_placeholder">p. ex. clube de leitura</string>
<string name="save">Guardar</string>
<string name="remove">Remover</string>
<string name="hint_long_press">Mantém premida uma conversa para adicionar uma pista privada.</string>
</resources>
9 changes: 9 additions & 0 deletions app/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,13 @@
<string name="listener_battery_exempt">Privee está exento de la optimización de batería de Android. Pueden seguir aplicándose otras restricciones del dispositivo.</string>
<string name="listener_settings_unavailable">Android no ha podido abrir estos ajustes. Abre manualmente los ajustes de la aplicación Privee.</string>
<string name="listener_preference_failed">No se ha podido guardar el ajuste de escucha en segundo plano. Vuelve a intentarlo.</string>
<string name="hint_add">Añadir pista</string>
<string name="hint_edit">Editar pista</string>
<string name="hint_title">Pista para %1$s</string>
<string name="hint_advice">Solo tú ves esta pista, y solo en este dispositivo. No escribas su nombre: cualquiera que desbloquee este dispositivo podría leerla.</string>
<string name="hint_label">Pista</string>
<string name="hint_placeholder">p. ej. club de lectura</string>
<string name="save">Guardar</string>
<string name="remove">Quitar</string>
<string name="hint_long_press">Mantén pulsada una conversación para añadir una pista privada.</string>
</resources>
9 changes: 9 additions & 0 deletions app/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,13 @@
<string name="listener_battery_exempt">Privee est exempté de l\'optimisation de la batterie Android. D\'autres restrictions de l\'appareil peuvent toujours s\'appliquer.</string>
<string name="listener_settings_unavailable">Android n\'a pas pu ouvrir ces paramètres. Ouvrez manuellement les paramètres de l\'application Privee.</string>
<string name="listener_preference_failed">Impossible d\'enregistrer le paramètre d\'écoute en arrière-plan. Réessayez.</string>
<string name="hint_add">Ajouter un indice</string>
<string name="hint_edit">Modifier l\'indice</string>
<string name="hint_title">Indice pour %1$s</string>
<string name="hint_advice">Vous seul voyez cet indice, et uniquement sur cet appareil. N\'écrivez pas son nom : toute personne qui déverrouille cet appareil pourrait le lire.</string>
<string name="hint_label">Indice</string>
<string name="hint_placeholder">ex. club de lecture</string>
<string name="save">Enregistrer</string>
<string name="remove">Supprimer</string>
<string name="hint_long_press">Appuyez longuement sur une conversation pour ajouter un indice privé.</string>
</resources>
9 changes: 9 additions & 0 deletions app/src/main/res/values-it/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,13 @@
<string name="listener_battery_exempt">Privee è esclusa dall\'ottimizzazione della batteria Android. Possono comunque applicarsi altre restrizioni del dispositivo.</string>
<string name="listener_settings_unavailable">Android non ha potuto aprire queste impostazioni. Apri manualmente le impostazioni dell\'app Privee.</string>
<string name="listener_preference_failed">Impossibile salvare l\'impostazione di ascolto in background. Riprova.</string>
<string name="hint_add">Aggiungi nota</string>
<string name="hint_edit">Modifica nota</string>
<string name="hint_title">Nota per %1$s</string>
<string name="hint_advice">Solo tu vedi questa nota, e solo su questo dispositivo. Non scrivere il suo nome: chiunque sblocchi questo dispositivo potrebbe leggerla.</string>
<string name="hint_label">Nota</string>
<string name="hint_placeholder">es. club del libro</string>
<string name="save">Salva</string>
<string name="remove">Rimuovi</string>
<string name="hint_long_press">Tieni premuta una conversazione per aggiungere una nota privata.</string>
</resources>
Loading
Loading