android: remove wallet menu and continue chat/settings localization
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package ru.daemonlord.messenger.ui.chat
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
@@ -67,6 +68,7 @@ import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
@@ -81,7 +83,9 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -93,7 +97,10 @@ import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
@@ -106,6 +113,7 @@ import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Forward
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
@@ -125,10 +133,10 @@ import androidx.compose.material.icons.filled.RadioButtonUnchecked
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.Wallpaper
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Image
|
||||
import androidx.compose.material.icons.filled.Link
|
||||
import androidx.compose.material.icons.filled.Wallpaper
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.FormatBold
|
||||
@@ -141,6 +149,7 @@ import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material.icons.automirrored.filled.Reply
|
||||
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
|
||||
import coil.compose.AsyncImage
|
||||
import ru.daemonlord.messenger.R
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -278,6 +287,9 @@ fun ChatRoute(
|
||||
onInlineSearchChanged = viewModel::onInlineSearchChanged,
|
||||
onJumpInlineSearch = viewModel::jumpInlineSearch,
|
||||
onVisibleIncomingMessageId = viewModel::onVisibleIncomingMessageId,
|
||||
onToggleChatNotifications = viewModel::onToggleChatNotifications,
|
||||
onClearHistory = viewModel::onClearHistory,
|
||||
onDeleteChat = viewModel::onDeleteOrLeaveChat,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -312,8 +324,12 @@ fun ChatScreen(
|
||||
onInlineSearchChanged: (String) -> Unit,
|
||||
onJumpInlineSearch: (Boolean) -> Unit,
|
||||
onVisibleIncomingMessageId: (Long?) -> Unit,
|
||||
onToggleChatNotifications: () -> Unit,
|
||||
onClearHistory: () -> Unit,
|
||||
onDeleteChat: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val view = LocalView.current
|
||||
val listState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
val allImageUrls = remember(state.messages) {
|
||||
@@ -344,7 +360,25 @@ fun ChatScreen(
|
||||
.distinct()
|
||||
}
|
||||
val isChannelChat = state.chatType.equals("channel", ignoreCase = true)
|
||||
val isPrivateChat = state.chatType.equals("private", ignoreCase = true)
|
||||
val timelineItems = remember(state.messages) { buildChatTimelineItems(state.messages) }
|
||||
val senderNameByUserId = remember(state.messages) {
|
||||
state.messages
|
||||
.mapNotNull { message ->
|
||||
val name = message.senderDisplayName?.trim().orEmpty()
|
||||
if (name.isBlank()) null else message.senderId to name
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
val replyAuthorByMessageId = remember(state.messages, senderNameByUserId) {
|
||||
state.messages.associate { message ->
|
||||
val resolved = message.senderDisplayName
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: senderNameByUserId[message.senderId]
|
||||
?: "Unknown user"
|
||||
message.id to resolved
|
||||
}
|
||||
}
|
||||
var didInitialAutoScroll by remember(state.chatId) { mutableStateOf(false) }
|
||||
var viewerImageIndex by remember { mutableStateOf<Int?>(null) }
|
||||
var viewerVideoUrl by remember { mutableStateOf<String?>(null) }
|
||||
@@ -365,6 +399,8 @@ fun ChatScreen(
|
||||
var giphyErrorMessage by remember { mutableStateOf<String?>(null) }
|
||||
var isPickerSending by remember { mutableStateOf(false) }
|
||||
var showChatMenu by remember { mutableStateOf(false) }
|
||||
var showClearHistoryDialog by remember { mutableStateOf(false) }
|
||||
var showDeleteChatDialog by remember { mutableStateOf(false) }
|
||||
var showChatInfoSheet by remember { mutableStateOf(false) }
|
||||
var chatInfoTab by remember { mutableStateOf(ChatInfoTab.Media) }
|
||||
var composerValue by remember { mutableStateOf(TextFieldValue(state.inputText)) }
|
||||
@@ -393,6 +429,22 @@ fun ChatScreen(
|
||||
lastVisibleIndex < lastIndex - 1
|
||||
}
|
||||
}
|
||||
val isLightTheme = MaterialTheme.colorScheme.background.luminance() > 0.5f
|
||||
val chatTopBarColor = if (isLightTheme) {
|
||||
MaterialTheme.colorScheme.surface.copy(alpha = 0.98f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surface
|
||||
.copy(alpha = 0.92f)
|
||||
.compositeOver(Color(0xFF1B1A22))
|
||||
}
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (context as? Activity)?.window ?: return@SideEffect
|
||||
window.statusBarColor = chatTopBarColor.toArgb()
|
||||
WindowCompat.getInsetsController(window, view)?.isAppearanceLightStatusBars =
|
||||
chatTopBarColor.luminance() > 0.5f
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state.isRecordingVoice) {
|
||||
if (!state.isRecordingVoice) return@LaunchedEffect
|
||||
@@ -439,6 +491,11 @@ fun ChatScreen(
|
||||
onInlineSearchChanged("")
|
||||
}
|
||||
}
|
||||
LaunchedEffect(state.chatDeletedNonce) {
|
||||
if (state.chatDeletedNonce > 0L) {
|
||||
onBack()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(emojiPickerTab, gifSearchQuery, giphyApiKey) {
|
||||
if (emojiPickerTab != ComposerPickerTab.Gif) return@LaunchedEffect
|
||||
val query = gifSearchQuery.trim()
|
||||
@@ -486,11 +543,19 @@ fun ChatScreen(
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
colors = if (isLightTheme) {
|
||||
listOf(
|
||||
Color(0xFFE1DCEC),
|
||||
Color(0xFFD8D2E4),
|
||||
Color(0xFFCFC8DC),
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
Color(0xFF1B1A22),
|
||||
Color(0xFF242034),
|
||||
Color(0xFF1A202A),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||
@@ -500,7 +565,7 @@ fun ChatScreen(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.92f))
|
||||
.background(chatTopBarColor)
|
||||
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
@@ -533,7 +598,7 @@ fun ChatScreen(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.9f))
|
||||
.background(chatTopBarColor)
|
||||
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
@@ -610,15 +675,15 @@ fun ChatScreen(
|
||||
onDismissRequest = { showChatMenu = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Chat info") },
|
||||
leadingIcon = { Icon(Icons.Filled.Info, contentDescription = null) },
|
||||
text = { Text(stringResource(id = R.string.chat_menu_notifications)) },
|
||||
leadingIcon = { Icon(Icons.Filled.Notifications, contentDescription = null) },
|
||||
onClick = {
|
||||
showChatMenu = false
|
||||
showChatInfoSheet = true
|
||||
onToggleChatNotifications()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Search") },
|
||||
text = { Text(stringResource(id = R.string.chat_menu_search)) },
|
||||
leadingIcon = { Icon(Icons.Filled.Search, contentDescription = null) },
|
||||
onClick = {
|
||||
showChatMenu = false
|
||||
@@ -626,19 +691,28 @@ fun ChatScreen(
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Change wallpaper") },
|
||||
text = { Text(stringResource(id = R.string.chat_menu_change_wallpaper)) },
|
||||
leadingIcon = { Icon(Icons.Filled.Wallpaper, contentDescription = null) },
|
||||
onClick = { showChatMenu = false },
|
||||
onClick = {
|
||||
showChatMenu = false
|
||||
Toast.makeText(context, context.getString(R.string.chat_wallpaper_coming_soon), Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Notifications") },
|
||||
leadingIcon = { Icon(Icons.Filled.Notifications, contentDescription = null) },
|
||||
onClick = { showChatMenu = false },
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Clear history") },
|
||||
text = { Text(stringResource(id = R.string.chat_menu_clear_history)) },
|
||||
leadingIcon = { Icon(Icons.Filled.DeleteOutline, contentDescription = null) },
|
||||
onClick = { showChatMenu = false },
|
||||
onClick = {
|
||||
showChatMenu = false
|
||||
showClearHistoryDialog = true
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (isPrivateChat) stringResource(id = R.string.chat_delete_dialog) else stringResource(id = R.string.chat_leave_delete)) },
|
||||
leadingIcon = { Icon(Icons.Filled.DeleteOutline, contentDescription = null) },
|
||||
onClick = {
|
||||
showChatMenu = false
|
||||
showDeleteChatDialog = true
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -657,7 +731,7 @@ fun ChatScreen(
|
||||
value = state.inlineSearchQuery,
|
||||
onValueChange = onInlineSearchChanged,
|
||||
modifier = Modifier.weight(1f),
|
||||
label = { Text("Search in chat") },
|
||||
label = { Text(stringResource(id = R.string.chat_search_in_chat)) },
|
||||
singleLine = true,
|
||||
)
|
||||
IconButton(
|
||||
@@ -678,7 +752,7 @@ fun ChatScreen(
|
||||
}
|
||||
if (showInlineSearch && state.inlineSearchMatches.isNotEmpty()) {
|
||||
Text(
|
||||
text = "Matches: ${state.inlineSearchMatches.size}",
|
||||
text = stringResource(id = R.string.chat_search_matches, state.inlineSearchMatches.size),
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
@@ -702,7 +776,7 @@ fun ChatScreen(
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Pinned message",
|
||||
text = stringResource(id = R.string.chat_pinned_message),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
@@ -823,6 +897,8 @@ fun ChatScreen(
|
||||
isSelected = isSelected,
|
||||
isMultiSelecting = state.actionState.mode == MessageSelectionMode.MULTI,
|
||||
isInlineHighlighted = state.highlightedMessageId == message.id,
|
||||
senderNameByUserId = senderNameByUserId,
|
||||
replyAuthorByMessageId = replyAuthorByMessageId,
|
||||
reactions = state.reactionByMessageId[message.id].orEmpty(),
|
||||
onAttachmentImageClick = { imageUrl ->
|
||||
val idx = allImageUrls.indexOf(imageUrl)
|
||||
@@ -962,7 +1038,7 @@ fun ChatScreen(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Filled.Reply, contentDescription = null)
|
||||
Text("Reply", style = MaterialTheme.typography.bodyLarge)
|
||||
Text(stringResource(id = R.string.chat_action_reply), style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
@@ -984,7 +1060,7 @@ fun ChatScreen(
|
||||
) {
|
||||
Icon(imageVector = Icons.Filled.Edit, contentDescription = null)
|
||||
Text(
|
||||
"Edit",
|
||||
stringResource(id = R.string.chat_action_edit),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = if (state.selectedCanEdit) {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
@@ -1013,7 +1089,7 @@ fun ChatScreen(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Filled.Forward, contentDescription = null)
|
||||
Text("Forward", style = MaterialTheme.typography.bodyLarge)
|
||||
Text(stringResource(id = R.string.chat_action_forward), style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
@@ -1041,7 +1117,7 @@ fun ChatScreen(
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
Text(
|
||||
"Delete",
|
||||
stringResource(id = R.string.chat_action_delete),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
@@ -1053,7 +1129,7 @@ fun ChatScreen(
|
||||
onClearSelection()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) { Text("Close") }
|
||||
) { Text(stringResource(id = R.string.common_close)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1072,14 +1148,14 @@ fun ChatScreen(
|
||||
) {
|
||||
Text(
|
||||
text = if (state.forwardingMessageIds.size == 1) {
|
||||
"Forward message #${state.forwardingMessageIds.first()}"
|
||||
stringResource(id = R.string.chat_forward_one, state.forwardingMessageIds.first())
|
||||
} else {
|
||||
"Forward ${state.forwardingMessageIds.size} messages"
|
||||
stringResource(id = R.string.chat_forward_many, state.forwardingMessageIds.size)
|
||||
},
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
if (state.availableForwardTargets.isEmpty()) {
|
||||
Text("No available chats")
|
||||
Text(stringResource(id = R.string.chat_no_available_chats))
|
||||
} else {
|
||||
state.availableForwardTargets.forEach { target ->
|
||||
Button(
|
||||
@@ -1096,7 +1172,7 @@ fun ChatScreen(
|
||||
enabled = !state.isForwarding,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(if (state.isForwarding) "Forwarding..." else "Cancel")
|
||||
Text(if (state.isForwarding) stringResource(id = R.string.chat_forwarding) else stringResource(id = R.string.common_cancel))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1262,9 +1338,14 @@ fun ChatScreen(
|
||||
|
||||
if (state.replyToMessage != null || state.editingMessage != null) {
|
||||
val header = if (state.editingMessage != null) {
|
||||
"Editing message #${state.editingMessage.id}"
|
||||
stringResource(id = R.string.chat_editing_message, state.editingMessage.id)
|
||||
} else {
|
||||
"Reply to ${state.replyToMessage?.senderDisplayName ?: "#${state.replyToMessage?.id}"}"
|
||||
"${stringResource(id = R.string.chat_reply_to)} ${
|
||||
state.replyToMessage?.senderDisplayName
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: state.replyToMessage?.senderId?.let { senderNameByUserId[it] }
|
||||
?: stringResource(id = R.string.common_unknown_user)
|
||||
}"
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -1275,7 +1356,7 @@ fun ChatScreen(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(text = header, style = MaterialTheme.typography.bodySmall)
|
||||
Button(onClick = onCancelComposeAction) { Text("Cancel") }
|
||||
Button(onClick = onCancelComposeAction) { Text(stringResource(id = R.string.common_cancel)) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1288,6 +1369,7 @@ fun ChatScreen(
|
||||
)
|
||||
} else if (isChannelChat && !state.canSendMessages) {
|
||||
ChannelReadOnlyBar(
|
||||
onToggleNotifications = onToggleChatNotifications,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
@@ -1417,7 +1499,8 @@ fun ChatScreen(
|
||||
onInputChanged(it.text)
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
placeholder = { Text("Message") },
|
||||
placeholder = { Text(stringResource(id = R.string.chat_message_placeholder)) },
|
||||
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
maxLines = 4,
|
||||
colors = TextFieldDefaults.colors(
|
||||
@@ -1486,13 +1569,13 @@ fun ChatScreen(
|
||||
if (showDeleteDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteDialog = false },
|
||||
title = { Text("Delete message") },
|
||||
title = { Text(stringResource(id = R.string.chat_delete_message_title)) },
|
||||
text = {
|
||||
Text(
|
||||
if (pendingDeleteForAll) {
|
||||
"Delete selected message for everyone?"
|
||||
stringResource(id = R.string.chat_delete_message_for_everyone)
|
||||
} else {
|
||||
"Delete selected message(s) for you?"
|
||||
stringResource(id = R.string.chat_delete_message_for_me)
|
||||
}
|
||||
)
|
||||
},
|
||||
@@ -1503,7 +1586,7 @@ fun ChatScreen(
|
||||
showDeleteDialog = false
|
||||
pendingDeleteForAll = false
|
||||
},
|
||||
) { Text("Delete") }
|
||||
) { Text(stringResource(id = R.string.common_delete)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
@@ -1511,7 +1594,51 @@ fun ChatScreen(
|
||||
showDeleteDialog = false
|
||||
pendingDeleteForAll = false
|
||||
},
|
||||
) { Text("Cancel") }
|
||||
) { Text(stringResource(id = R.string.common_cancel)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
if (showClearHistoryDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showClearHistoryDialog = false },
|
||||
title = { Text(stringResource(id = R.string.chat_menu_clear_history)) },
|
||||
text = { Text(stringResource(id = R.string.chat_clear_history_confirm)) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onClearHistory()
|
||||
showClearHistoryDialog = false
|
||||
},
|
||||
) { Text(stringResource(id = R.string.chat_clear)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showClearHistoryDialog = false }) { Text(stringResource(id = R.string.common_cancel)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
if (showDeleteChatDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteChatDialog = false },
|
||||
title = { Text(if (isPrivateChat) stringResource(id = R.string.chat_delete_dialog) else stringResource(id = R.string.chat_leave_chat)) },
|
||||
text = {
|
||||
Text(
|
||||
if (isPrivateChat) {
|
||||
stringResource(id = R.string.chat_delete_dialog_confirm)
|
||||
} else {
|
||||
stringResource(id = R.string.chat_leave_delete_confirm)
|
||||
}
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDeleteChat()
|
||||
showDeleteChatDialog = false
|
||||
},
|
||||
) { Text(if (isPrivateChat) stringResource(id = R.string.common_delete) else stringResource(id = R.string.chat_leave)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDeleteChatDialog = false }) { Text(stringResource(id = R.string.common_cancel)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1577,7 +1704,7 @@ fun ChatScreen(
|
||||
}
|
||||
},
|
||||
enabled = currentIndex > 0,
|
||||
) { Text("Prev") }
|
||||
) { Text(stringResource(id = R.string.chat_search_prev)) }
|
||||
Button(
|
||||
onClick = {
|
||||
viewerImageIndex = if (allImageUrls.isEmpty()) null else {
|
||||
@@ -1585,7 +1712,7 @@ fun ChatScreen(
|
||||
}
|
||||
},
|
||||
enabled = currentIndex < allImageUrls.lastIndex,
|
||||
) { Text("Next") }
|
||||
) { Text(stringResource(id = R.string.chat_search_next)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1826,7 +1953,7 @@ fun ChatScreen(
|
||||
onValueChange = { gifSearchQuery = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
placeholder = { Text("Search GIFs") },
|
||||
placeholder = { Text(stringResource(id = R.string.chat_search_gifs)) },
|
||||
)
|
||||
when {
|
||||
isGiphyLoading -> {
|
||||
@@ -1945,6 +2072,8 @@ private fun MessageBubble(
|
||||
isSelected: Boolean,
|
||||
isMultiSelecting: Boolean,
|
||||
isInlineHighlighted: Boolean,
|
||||
senderNameByUserId: Map<Long, String>,
|
||||
replyAuthorByMessageId: Map<Long, String>,
|
||||
reactions: List<ru.daemonlord.messenger.domain.message.model.MessageReaction>,
|
||||
onAttachmentImageClick: (String) -> Unit,
|
||||
onAttachmentVideoClick: (String) -> Unit,
|
||||
@@ -1981,6 +2110,20 @@ private fun MessageBubble(
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
val senderName = message.senderDisplayName
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: senderNameByUserId[message.senderId]
|
||||
val legacyTextUrl = message.text?.trim()?.takeIf { it.startsWith("http", ignoreCase = true) }
|
||||
val hasLegacyStickerImage = message.attachments.isEmpty() &&
|
||||
message.type.equals("image", ignoreCase = true) &&
|
||||
!legacyTextUrl.isNullOrBlank() &&
|
||||
isStickerAsset(fileUrl = legacyTextUrl, fileType = "image/url")
|
||||
val singleImageAttachment = message.attachments.singleOrNull()?.takeIf {
|
||||
it.fileType.lowercase().startsWith("image/")
|
||||
}
|
||||
val hasSingleStickerAttachment = singleImageAttachment?.let {
|
||||
isStickerAsset(fileUrl = it.fileUrl, fileType = it.fileType)
|
||||
} == true || hasLegacyStickerImage
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -1996,9 +2139,21 @@ private fun MessageBubble(
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(if (renderAsChannelPost) 0.94f else 0.8f)
|
||||
.widthIn(min = if (renderAsChannelPost) 120.dp else 82.dp)
|
||||
.background(
|
||||
.fillMaxWidth(
|
||||
if (hasSingleStickerAttachment) {
|
||||
if (renderAsChannelPost) 0.58f else 0.52f
|
||||
} else if (renderAsChannelPost) {
|
||||
0.94f
|
||||
} else {
|
||||
0.8f
|
||||
},
|
||||
)
|
||||
.widthIn(min = if (hasSingleStickerAttachment) 96.dp else if (renderAsChannelPost) 120.dp else 82.dp)
|
||||
.then(
|
||||
if (hasSingleStickerAttachment) {
|
||||
Modifier
|
||||
} else {
|
||||
Modifier.background(
|
||||
color = when {
|
||||
isSelected -> MaterialTheme.colorScheme.tertiaryContainer
|
||||
isInlineHighlighted -> MaterialTheme.colorScheme.secondaryContainer
|
||||
@@ -2006,16 +2161,21 @@ private fun MessageBubble(
|
||||
},
|
||||
shape = bubbleShape,
|
||||
)
|
||||
},
|
||||
)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongPress,
|
||||
)
|
||||
.padding(horizontal = if (renderAsChannelPost) 11.dp else 10.dp, vertical = 7.dp),
|
||||
.padding(
|
||||
horizontal = if (hasSingleStickerAttachment) 0.dp else if (renderAsChannelPost) 11.dp else 10.dp,
|
||||
vertical = if (hasSingleStickerAttachment) 2.dp else 7.dp,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(3.dp),
|
||||
) {
|
||||
if ((!alignAsOutgoing || renderAsChannelPost) && !message.senderDisplayName.isNullOrBlank()) {
|
||||
if ((!alignAsOutgoing || renderAsChannelPost) && !senderName.isNullOrBlank()) {
|
||||
Text(
|
||||
text = message.senderDisplayName,
|
||||
text = senderName,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
@@ -2031,7 +2191,10 @@ private fun MessageBubble(
|
||||
}
|
||||
|
||||
if (message.replyToMessageId != null) {
|
||||
val replyAuthor = message.replyPreviewSenderName?.takeIf { it.isNotBlank() } ?: "#${message.replyToMessageId}"
|
||||
val replyAuthor = message.replyPreviewSenderName
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: replyAuthorByMessageId[message.replyToMessageId]
|
||||
?: "Unknown user"
|
||||
val replySnippet = message.replyPreviewText?.takeIf { it.isNotBlank() } ?: "[media]"
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -2092,15 +2255,25 @@ private fun MessageBubble(
|
||||
)
|
||||
}
|
||||
|
||||
if (isLegacyImageUrlMessage && textUrl != null) {
|
||||
val badgeLabel = mediaBadgeLabel(fileType = "image/url", url = textUrl)
|
||||
if (isLegacyImageUrlMessage) {
|
||||
val isLegacyStickerMessage = isStickerAsset(fileUrl = textUrl, fileType = "image/url")
|
||||
val badgeLabel = if (isLegacyStickerMessage) null else mediaBadgeLabel(fileType = "image/url", url = textUrl)
|
||||
val openable = !isLegacyStickerMessage && badgeLabel == null
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (isLegacyStickerMessage) {
|
||||
Modifier
|
||||
.size(176.dp)
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(188.dp)
|
||||
},
|
||||
)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.let { base ->
|
||||
if (isGifOrStickerLegacyImage) {
|
||||
if (isGifOrStickerLegacyImage || !openable) {
|
||||
base
|
||||
} else {
|
||||
base.clickable { onAttachmentImageClick(textUrl) }
|
||||
@@ -2111,7 +2284,7 @@ private fun MessageBubble(
|
||||
model = textUrl,
|
||||
contentDescription = "Image",
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop,
|
||||
contentScale = if (isLegacyStickerMessage) ContentScale.Fit else ContentScale.Crop,
|
||||
)
|
||||
if (!badgeLabel.isNullOrBlank()) {
|
||||
MediaTypeBadge(
|
||||
@@ -2124,7 +2297,7 @@ private fun MessageBubble(
|
||||
}
|
||||
}
|
||||
|
||||
if (isLegacyVideoUrlMessage && textUrl != null) {
|
||||
if (isLegacyVideoUrlMessage) {
|
||||
VideoAttachmentCard(
|
||||
url = textUrl,
|
||||
fileType = message.type,
|
||||
@@ -2157,12 +2330,21 @@ private fun MessageBubble(
|
||||
if (imageAttachments.isNotEmpty()) {
|
||||
if (imageAttachments.size == 1) {
|
||||
val single = imageAttachments.first()
|
||||
val badgeLabel = mediaBadgeLabel(fileType = single.fileType, url = single.fileUrl)
|
||||
val openable = badgeLabel == null
|
||||
val isStickerImage = isStickerAsset(fileUrl = single.fileUrl, fileType = single.fileType)
|
||||
val badgeLabel = if (isStickerImage) null else mediaBadgeLabel(fileType = single.fileType, url = single.fileUrl)
|
||||
val openable = !isStickerImage && badgeLabel == null
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (isStickerImage) {
|
||||
Modifier
|
||||
.size(176.dp)
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(188.dp)
|
||||
},
|
||||
)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.let { base ->
|
||||
if (openable) base.clickable { onAttachmentImageClick(single.fileUrl) } else base
|
||||
@@ -2172,7 +2354,7 @@ private fun MessageBubble(
|
||||
model = single.fileUrl,
|
||||
contentDescription = "Image",
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop,
|
||||
contentScale = if (isStickerImage) ContentScale.Fit else ContentScale.Crop,
|
||||
)
|
||||
if (!badgeLabel.isNullOrBlank()) {
|
||||
MediaTypeBadge(
|
||||
@@ -2279,13 +2461,17 @@ private fun MessageBubble(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
val status = when (message.status) {
|
||||
val status = if (isOutgoing && !renderAsChannelPost) {
|
||||
when (message.status) {
|
||||
"read" -> " ✓✓"
|
||||
"delivered" -> " ✓✓"
|
||||
"sent" -> " ✓"
|
||||
"pending" -> " …"
|
||||
else -> ""
|
||||
}
|
||||
} else {
|
||||
""
|
||||
}
|
||||
Text(
|
||||
text = if (renderAsChannelPost) {
|
||||
formatMessageTime(message.createdAt)
|
||||
@@ -2362,7 +2548,7 @@ private fun CircleVideoAttachmentPlayer(url: String) {
|
||||
}
|
||||
},
|
||||
)
|
||||
Text("Circle video", style = MaterialTheme.typography.labelSmall)
|
||||
Text(stringResource(id = R.string.chat_circle_video), style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2488,43 +2674,28 @@ private fun DaySeparatorChip(label: String) {
|
||||
|
||||
@Composable
|
||||
private fun ChannelReadOnlyBar(
|
||||
onToggleNotifications: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(0.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Surface(
|
||||
shape = CircleShape,
|
||||
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.94f),
|
||||
modifier = Modifier.size(46.dp),
|
||||
) {
|
||||
IconButton(onClick = { }) {
|
||||
Icon(imageVector = Icons.Filled.Search, contentDescription = "Search in channel")
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
shape = RoundedCornerShape(999.dp),
|
||||
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.94f),
|
||||
modifier = Modifier.weight(1f),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clickable { onToggleNotifications() },
|
||||
) {
|
||||
Text(
|
||||
text = "Включить звук",
|
||||
text = stringResource(id = R.string.chat_enable_sound),
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
||||
)
|
||||
}
|
||||
Surface(
|
||||
shape = CircleShape,
|
||||
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.94f),
|
||||
modifier = Modifier.size(46.dp),
|
||||
) {
|
||||
IconButton(onClick = { }) {
|
||||
Icon(imageVector = Icons.Filled.Notifications, contentDescription = "Channel notifications")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2553,7 +2724,7 @@ private fun MultiSelectActionBar(
|
||||
enabled = canReply,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Reply")
|
||||
Text(stringResource(id = R.string.chat_action_reply))
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
@@ -2566,7 +2737,7 @@ private fun MultiSelectActionBar(
|
||||
enabled = selectedCount > 0,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Forward")
|
||||
Text(stringResource(id = R.string.chat_action_forward))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2578,7 +2749,7 @@ private fun parseMessageLocalDate(createdAt: String): LocalDate? {
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private val daySeparatorFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("d MMMM", Locale("ru"))
|
||||
private val daySeparatorFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("d MMMM", Locale.getDefault())
|
||||
|
||||
private fun formatDateSeparatorLabel(date: LocalDate): String {
|
||||
val today = LocalDate.now()
|
||||
@@ -2630,8 +2801,8 @@ private fun VoiceRecordingStatusRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(onClick = onCancel) { Text("Cancel") }
|
||||
Button(onClick = onSend) { Text("Send") }
|
||||
Button(onClick = onCancel) { Text(stringResource(id = R.string.common_cancel)) }
|
||||
Button(onClick = onSend) { Text(stringResource(id = R.string.common_send)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3521,7 +3692,16 @@ private fun isGifLikeUrl(url: String): Boolean {
|
||||
private fun isStickerLikeUrl(url: String): Boolean {
|
||||
if (url.isBlank()) return false
|
||||
val normalized = url.lowercase(Locale.getDefault())
|
||||
return normalized.contains("twemoji") || normalized.endsWith(".webp")
|
||||
return normalized.contains("twemoji") ||
|
||||
normalized.endsWith(".webp") ||
|
||||
normalized.contains("sticker_") ||
|
||||
normalized.contains("/stickers/")
|
||||
}
|
||||
|
||||
private fun isStickerAsset(fileUrl: String, fileType: String): Boolean {
|
||||
val normalizedType = fileType.lowercase(Locale.getDefault())
|
||||
if (normalizedType.contains("webp")) return true
|
||||
return isStickerLikeUrl(fileUrl)
|
||||
}
|
||||
|
||||
private fun mediaBadgeLabel(fileType: String, url: String): String? {
|
||||
|
||||
@@ -30,9 +30,11 @@ import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.AssistChipDefaults
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -40,6 +42,7 @@ import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -56,6 +59,7 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -68,17 +72,21 @@ import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.DoneAll
|
||||
import androidx.compose.material.icons.filled.Groups
|
||||
import androidx.compose.material.icons.filled.Inventory2
|
||||
import androidx.compose.material.icons.filled.FolderOpen
|
||||
import androidx.compose.material.icons.filled.LightMode
|
||||
import androidx.compose.material.icons.filled.BookmarkBorder
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.NotificationsOff
|
||||
import androidx.compose.material.icons.filled.PushPin
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.DragHandle
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import coil.compose.AsyncImage
|
||||
import ru.daemonlord.messenger.R
|
||||
import ru.daemonlord.messenger.domain.chat.model.ChatItem
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
@@ -134,6 +142,7 @@ fun ChatListRoute(
|
||||
onUpdateChatProfile = viewModel::updateChatProfile,
|
||||
onClearChat = viewModel::clearChat,
|
||||
onDeleteChat = viewModel::deleteChatForMe,
|
||||
onDeleteChatForAll = viewModel::deleteChatForAll,
|
||||
onToggleChatMute = viewModel::toggleChatMute,
|
||||
onSelectManageChat = viewModel::onManagementChatSelected,
|
||||
onCreateInvite = viewModel::createInvite,
|
||||
@@ -173,6 +182,7 @@ fun ChatListScreen(
|
||||
onUpdateChatProfile: (Long, String?, String?) -> Unit,
|
||||
onClearChat: (Long) -> Unit,
|
||||
onDeleteChat: (Long) -> Unit,
|
||||
onDeleteChatForAll: (Long) -> Unit,
|
||||
onToggleChatMute: (Long) -> Unit,
|
||||
onSelectManageChat: (Long?) -> Unit,
|
||||
onCreateInvite: (Long) -> Unit,
|
||||
@@ -200,6 +210,13 @@ fun ChatListScreen(
|
||||
var selectedManageChatIdText by remember { mutableStateOf("") }
|
||||
var manageUserIdText by remember { mutableStateOf("") }
|
||||
var manageRoleText by remember { mutableStateOf("member") }
|
||||
var showCreateGroupDialog by remember { mutableStateOf(false) }
|
||||
var showCreateChannelDialog by remember { mutableStateOf(false) }
|
||||
var quickCreateGroupTitle by remember { mutableStateOf("") }
|
||||
var quickCreateChannelTitle by remember { mutableStateOf("") }
|
||||
var quickCreateChannelHandle by remember { mutableStateOf("") }
|
||||
var showDeleteChatsDialog by remember { mutableStateOf(false) }
|
||||
var deleteSelectedForAll by remember { mutableStateOf(false) }
|
||||
val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840
|
||||
val listState = rememberLazyListState()
|
||||
val selectedChats = remember(state.chats, selectedChatIds) {
|
||||
@@ -282,9 +299,9 @@ fun ChatListScreen(
|
||||
when {
|
||||
selectedChatIds.isNotEmpty() -> selectedChatIds.size.toString()
|
||||
isSearchMode -> ""
|
||||
state.isConnecting -> "Connecting..."
|
||||
state.selectedTab == ChatTab.ARCHIVED -> "Archived"
|
||||
else -> "Chats"
|
||||
state.isConnecting -> stringResource(id = R.string.chats_connecting)
|
||||
state.selectedTab == ChatTab.ARCHIVED -> stringResource(id = R.string.chats_archived)
|
||||
else -> stringResource(id = R.string.nav_chats)
|
||||
}
|
||||
)
|
||||
},
|
||||
@@ -311,8 +328,7 @@ fun ChatListScreen(
|
||||
)
|
||||
}
|
||||
IconButton(onClick = {
|
||||
selectedChatIds.forEach { chatId -> onDeleteChat(chatId) }
|
||||
selectedChatIds = emptySet()
|
||||
showDeleteChatsDialog = true
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Delete,
|
||||
@@ -394,45 +410,45 @@ fun ChatListScreen(
|
||||
onDismissRequest = { showDefaultMenu = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Day mode") },
|
||||
text = {
|
||||
val isNight = AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES
|
||||
Text(
|
||||
if (isNight) {
|
||||
stringResource(id = R.string.menu_day_mode)
|
||||
} else {
|
||||
stringResource(id = R.string.menu_night_mode)
|
||||
}
|
||||
)
|
||||
},
|
||||
leadingIcon = { Icon(Icons.Filled.LightMode, contentDescription = null) },
|
||||
onClick = {
|
||||
showDefaultMenu = false
|
||||
Toast.makeText(context, "Theme switch in Settings.", Toast.LENGTH_SHORT).show()
|
||||
val isNight = AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES
|
||||
if (isNight) {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
Toast.makeText(context, context.getString(R.string.toast_day_mode_enabled), Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
Toast.makeText(context, context.getString(R.string.toast_night_mode_enabled), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Create group") },
|
||||
leadingIcon = { Icon(Icons.Filled.FolderOpen, contentDescription = null) },
|
||||
text = { Text(stringResource(id = R.string.menu_create_group)) },
|
||||
leadingIcon = { Icon(Icons.Filled.Groups, contentDescription = null) },
|
||||
onClick = {
|
||||
showDefaultMenu = false
|
||||
managementExpanded = true
|
||||
showCreateGroupDialog = true
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Create channel") },
|
||||
leadingIcon = { Icon(Icons.Filled.FolderOpen, contentDescription = null) },
|
||||
onClick = {
|
||||
showDefaultMenu = false
|
||||
managementExpanded = true
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Saved") },
|
||||
leadingIcon = { Icon(Icons.Filled.Inventory2, contentDescription = null) },
|
||||
text = { Text(stringResource(id = R.string.menu_saved)) },
|
||||
leadingIcon = { Icon(Icons.Filled.BookmarkBorder, contentDescription = null) },
|
||||
onClick = {
|
||||
showDefaultMenu = false
|
||||
onOpenSaved()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Proxy") },
|
||||
leadingIcon = { Icon(Icons.Filled.NotificationsOff, contentDescription = null) },
|
||||
onClick = {
|
||||
showDefaultMenu = false
|
||||
Toast.makeText(context, "Proxy settings will be added next.", Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -447,22 +463,22 @@ fun ChatListScreen(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
FilterChip(
|
||||
label = "All",
|
||||
label = stringResource(id = R.string.filter_all),
|
||||
selected = state.selectedFilter == ChatListFilter.ALL,
|
||||
onClick = { onFilterSelected(ChatListFilter.ALL) },
|
||||
)
|
||||
FilterChip(
|
||||
label = "People",
|
||||
label = stringResource(id = R.string.filter_people),
|
||||
selected = state.selectedFilter == ChatListFilter.PEOPLE,
|
||||
onClick = { onFilterSelected(ChatListFilter.PEOPLE) },
|
||||
)
|
||||
FilterChip(
|
||||
label = "Groups",
|
||||
label = stringResource(id = R.string.filter_groups),
|
||||
selected = state.selectedFilter == ChatListFilter.GROUPS,
|
||||
onClick = { onFilterSelected(ChatListFilter.GROUPS) },
|
||||
)
|
||||
FilterChip(
|
||||
label = "Channels",
|
||||
label = stringResource(id = R.string.filter_channels),
|
||||
selected = state.selectedFilter == ChatListFilter.CHANNELS,
|
||||
onClick = { onFilterSelected(ChatListFilter.CHANNELS) },
|
||||
)
|
||||
@@ -475,7 +491,7 @@ fun ChatListScreen(
|
||||
) {
|
||||
when {
|
||||
state.isLoading -> {
|
||||
CenterState(text = "Loading chats...", loading = true)
|
||||
CenterState(text = stringResource(id = R.string.chats_loading), loading = true)
|
||||
}
|
||||
|
||||
!state.errorMessage.isNullOrBlank() -> {
|
||||
@@ -483,7 +499,7 @@ fun ChatListScreen(
|
||||
}
|
||||
|
||||
state.chats.isEmpty() -> {
|
||||
CenterState(text = "No chats found", loading = false)
|
||||
CenterState(text = stringResource(id = R.string.chats_not_found), loading = false)
|
||||
}
|
||||
|
||||
else -> {
|
||||
@@ -759,6 +775,120 @@ fun ChatListScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (showCreateGroupDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showCreateGroupDialog = false },
|
||||
title = { Text("Create group") },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = quickCreateGroupTitle,
|
||||
onValueChange = { quickCreateGroupTitle = it },
|
||||
label = { Text("Group title") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val title = quickCreateGroupTitle.trim()
|
||||
if (title.isNotBlank()) {
|
||||
onCreateGroup(title, emptyList())
|
||||
showCreateGroupDialog = false
|
||||
quickCreateGroupTitle = ""
|
||||
}
|
||||
},
|
||||
) { Text("Create") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showCreateGroupDialog = false }) { Text("Cancel") }
|
||||
},
|
||||
)
|
||||
}
|
||||
if (showCreateChannelDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showCreateChannelDialog = false },
|
||||
title = { Text("Create channel") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(
|
||||
value = quickCreateChannelTitle,
|
||||
onValueChange = { quickCreateChannelTitle = it },
|
||||
label = { Text("Channel title") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = quickCreateChannelHandle,
|
||||
onValueChange = { quickCreateChannelHandle = it },
|
||||
label = { Text("Handle") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val title = quickCreateChannelTitle.trim()
|
||||
val handle = quickCreateChannelHandle.trim()
|
||||
if (title.isNotBlank() && handle.isNotBlank()) {
|
||||
onCreateChannel(title, handle, null)
|
||||
showCreateChannelDialog = false
|
||||
quickCreateChannelTitle = ""
|
||||
quickCreateChannelHandle = ""
|
||||
}
|
||||
},
|
||||
) { Text("Create") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showCreateChannelDialog = false }) { Text("Cancel") }
|
||||
},
|
||||
)
|
||||
}
|
||||
if (showDeleteChatsDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteChatsDialog = false },
|
||||
title = { Text("Delete selected chats") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Are you sure you want to delete selected chats?")
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { deleteSelectedForAll = !deleteSelectedForAll },
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Checkbox(
|
||||
checked = deleteSelectedForAll,
|
||||
onCheckedChange = { deleteSelectedForAll = it },
|
||||
)
|
||||
Text("Delete for all (where allowed)")
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
selectedChatIds.forEach { chatId ->
|
||||
if (deleteSelectedForAll) onDeleteChatForAll(chatId) else onDeleteChat(chatId)
|
||||
}
|
||||
selectedChatIds = emptySet()
|
||||
deleteSelectedForAll = false
|
||||
showDeleteChatsDialog = false
|
||||
},
|
||||
) { Text("Delete") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showDeleteChatsDialog = false
|
||||
deleteSelectedForAll = false
|
||||
},
|
||||
) { Text("Cancel") }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
130
android/app/src/main/res/values-ru/strings.xml
Normal file
130
android/app/src/main/res/values-ru/strings.xml
Normal file
@@ -0,0 +1,130 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Benya Messenger</string>
|
||||
<string name="nav_chats">Чаты</string>
|
||||
<string name="nav_contacts">Контакты</string>
|
||||
<string name="nav_settings">Настройки</string>
|
||||
<string name="nav_profile">Профиль</string>
|
||||
|
||||
<string name="chats_connecting">Подключение...</string>
|
||||
<string name="chats_archived">Архив</string>
|
||||
<string name="chats_loading">Загрузка чатов...</string>
|
||||
<string name="chats_not_found">Чаты не найдены</string>
|
||||
|
||||
<string name="filter_all">Все</string>
|
||||
<string name="filter_people">Люди</string>
|
||||
<string name="filter_groups">Группы</string>
|
||||
<string name="filter_channels">Каналы</string>
|
||||
|
||||
<string name="menu_day_mode">Дневной режим</string>
|
||||
<string name="menu_night_mode">Ночной режим</string>
|
||||
<string name="menu_create_group">Создать группу</string>
|
||||
<string name="menu_saved">Избранное</string>
|
||||
<string name="toast_day_mode_enabled">Включен дневной режим.</string>
|
||||
<string name="toast_night_mode_enabled">Включен ночной режим.</string>
|
||||
|
||||
<string name="common_cancel">Отмена</string>
|
||||
<string name="common_close">Закрыть</string>
|
||||
<string name="common_delete">Удалить</string>
|
||||
<string name="common_send">Отправить</string>
|
||||
<string name="common_unknown_user">Неизвестный пользователь</string>
|
||||
|
||||
<string name="chat_menu_notifications">Уведомления</string>
|
||||
<string name="chat_menu_search">Поиск</string>
|
||||
<string name="chat_menu_change_wallpaper">Изменить обои</string>
|
||||
<string name="chat_menu_clear_history">Очистить историю</string>
|
||||
<string name="chat_wallpaper_coming_soon">Смена обоев будет добавлена позже.</string>
|
||||
<string name="chat_delete_dialog">Удалить диалог</string>
|
||||
<string name="chat_leave_delete">Выйти и удалить чат</string>
|
||||
<string name="chat_leave_chat">Выйти из чата</string>
|
||||
<string name="chat_leave">Выйти</string>
|
||||
<string name="chat_search_in_chat">Поиск по чату</string>
|
||||
<string name="chat_search_prev">Назад</string>
|
||||
<string name="chat_search_next">Далее</string>
|
||||
<string name="chat_search_gifs">Поиск GIF</string>
|
||||
<string name="chat_search_matches">Совпадений: %1$d</string>
|
||||
<string name="chat_pinned_message">Закрепленное сообщение</string>
|
||||
<string name="chat_message_placeholder">Сообщение</string>
|
||||
<string name="chat_action_reply">Ответить</string>
|
||||
<string name="chat_action_edit">Изменить</string>
|
||||
<string name="chat_action_forward">Переслать</string>
|
||||
<string name="chat_action_delete">Удалить</string>
|
||||
<string name="chat_delete_message_title">Удалить сообщение</string>
|
||||
<string name="chat_forward_one">Переслать сообщение #%1$d</string>
|
||||
<string name="chat_forward_many">Переслать %1$d сообщений</string>
|
||||
<string name="chat_no_available_chats">Нет доступных чатов</string>
|
||||
<string name="chat_forwarding">Пересылка...</string>
|
||||
<string name="chat_editing_message">Редактирование сообщения #%1$d</string>
|
||||
<string name="chat_reply_to">Ответ</string>
|
||||
<string name="chat_delete_message_for_everyone">Удалить выбранное сообщение для всех?</string>
|
||||
<string name="chat_delete_message_for_me">Удалить выбранные сообщения у вас?</string>
|
||||
<string name="chat_clear_history_confirm">Удалить все сообщения в этом чате? Это действие нельзя отменить.</string>
|
||||
<string name="chat_clear">Очистить</string>
|
||||
<string name="chat_delete_dialog_confirm">Удалить диалог у вас? История будет очищена.</string>
|
||||
<string name="chat_leave_delete_confirm">Выйти из чата и убрать его из списка?</string>
|
||||
<string name="chat_enable_sound">Включить звук</string>
|
||||
<string name="chat_circle_video">Видеокружок</string>
|
||||
|
||||
<string name="settings_user_fallback">Пользователь</string>
|
||||
<string name="settings_accounts_header">АККАУНТЫ</string>
|
||||
<string name="settings_no_active_account">Нет активного аккаунта</string>
|
||||
<string name="settings_add_account">Добавить аккаунт</string>
|
||||
<string name="settings_folder_account">Аккаунт</string>
|
||||
<string name="settings_folder_chat">Настройки чатов</string>
|
||||
<string name="settings_folder_privacy">Конфиденциальность</string>
|
||||
<string name="settings_folder_notifications">Уведомления</string>
|
||||
<string name="settings_folder_data">Данные и память</string>
|
||||
<string name="settings_folder_folders">Папки с чатами</string>
|
||||
<string name="settings_folder_devices">Устройства</string>
|
||||
<string name="settings_folder_power">Энергосбережение</string>
|
||||
<string name="settings_folder_language">Язык</string>
|
||||
<string name="settings_account_subtitle">Номер, имя пользователя, «О себе»</string>
|
||||
<string name="settings_chat_subtitle">Обои, ночной режим, анимации</string>
|
||||
<string name="settings_privacy_subtitle">Время захода, устройства, ключи доступа</string>
|
||||
<string name="settings_notifications_subtitle">Звуки, звонки, счетчик сообщений</string>
|
||||
<string name="settings_data_subtitle">Настройки загрузки медиафайлов</string>
|
||||
<string name="settings_folders_subtitle">Сортировка чатов по папкам</string>
|
||||
<string name="settings_devices_subtitle">Управление активными сессиями</string>
|
||||
<string name="settings_power_subtitle">Экономия энергии при низком заряде</string>
|
||||
<string name="settings_placeholder_data">Этот раздел будет расширен на следующем шаге.</string>
|
||||
<string name="settings_placeholder_folders">Управление папками чатов будет добавлено на следующей итерации.</string>
|
||||
<string name="settings_placeholder_power">Настройки энергосбережения будут добавлены отдельным этапом.</string>
|
||||
<string name="settings_language_title">Язык приложения</string>
|
||||
<string name="settings_language_system_subtitle">Использовать язык устройства</string>
|
||||
<string name="settings_language_english_subtitle">Английский</string>
|
||||
<string name="language_system">Системный</string>
|
||||
<string name="language_russian">Русский</string>
|
||||
<string name="language_english">Английский</string>
|
||||
<string name="settings_accounts">Аккаунты</string>
|
||||
<string name="settings_active">Активный</string>
|
||||
<string name="settings_switch">Переключить</string>
|
||||
<string name="settings_open_profile">Открыть профиль</string>
|
||||
<string name="settings_logout">Выйти</string>
|
||||
<string name="settings_appearance">Оформление</string>
|
||||
<string name="theme_light">Светлая</string>
|
||||
<string name="theme_dark">Темная</string>
|
||||
<string name="theme_system">Системная</string>
|
||||
<string name="settings_enable_notifications">Включить уведомления</string>
|
||||
<string name="settings_show_preview">Показывать превью сообщений</string>
|
||||
<string name="settings_refresh_notifications">Обновить историю уведомлений</string>
|
||||
<string name="settings_recent_notifications">Последние уведомления</string>
|
||||
<string name="settings_no_notifications_yet">Пока нет серверных уведомлений.</string>
|
||||
<string name="privacy_private_messages">Личные сообщения</string>
|
||||
<string name="privacy_last_seen">Время захода</string>
|
||||
<string name="privacy_avatar">Аватар</string>
|
||||
<string name="privacy_group_invites">Приглашения в группы</string>
|
||||
<string name="privacy_save">Сохранить приватность</string>
|
||||
<string name="settings_sessions_security">Сессии и безопасность</string>
|
||||
<string name="settings_unknown_device">Неизвестное устройство</string>
|
||||
<string name="settings_unknown_ip">Неизвестный IP</string>
|
||||
<string name="settings_revoke">Отозвать</string>
|
||||
<string name="settings_revoke_all">Отозвать все сессии</string>
|
||||
<string name="settings_2fa_code">Код 2FA</string>
|
||||
<string name="settings_enable">Включить</string>
|
||||
<string name="settings_disable">Выключить</string>
|
||||
<string name="settings_recovery_regen_code">Код для регенерации recovery</string>
|
||||
<string name="settings_regenerate_recovery_codes">Сгенерировать recovery-коды заново</string>
|
||||
<string name="privacy_everyone">все</string>
|
||||
<string name="privacy_contacts">контакты</string>
|
||||
<string name="privacy_nobody">никто</string>
|
||||
</resources>
|
||||
@@ -1,4 +1,130 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Benya Messenger</string>
|
||||
<string name="nav_chats">Chats</string>
|
||||
<string name="nav_contacts">Contacts</string>
|
||||
<string name="nav_settings">Settings</string>
|
||||
<string name="nav_profile">Profile</string>
|
||||
|
||||
<string name="chats_connecting">Connecting...</string>
|
||||
<string name="chats_archived">Archived</string>
|
||||
<string name="chats_loading">Loading chats...</string>
|
||||
|
||||
<string name="filter_all">All</string>
|
||||
<string name="filter_people">People</string>
|
||||
<string name="filter_groups">Groups</string>
|
||||
<string name="filter_channels">Channels</string>
|
||||
|
||||
<string name="menu_day_mode">Day mode</string>
|
||||
<string name="menu_night_mode">Night mode</string>
|
||||
<string name="menu_create_group">Create group</string>
|
||||
<string name="menu_saved">Saved</string>
|
||||
<string name="toast_day_mode_enabled">Day mode enabled.</string>
|
||||
<string name="toast_night_mode_enabled">Night mode enabled.</string>
|
||||
<string name="chats_not_found">No chats found</string>
|
||||
|
||||
<string name="common_cancel">Cancel</string>
|
||||
<string name="common_close">Close</string>
|
||||
<string name="common_delete">Delete</string>
|
||||
<string name="common_send">Send</string>
|
||||
<string name="common_unknown_user">Unknown user</string>
|
||||
|
||||
<string name="chat_menu_notifications">Notifications</string>
|
||||
<string name="chat_menu_search">Search</string>
|
||||
<string name="chat_menu_change_wallpaper">Change wallpaper</string>
|
||||
<string name="chat_menu_clear_history">Clear history</string>
|
||||
<string name="chat_wallpaper_coming_soon">Wallpaper change will be added later.</string>
|
||||
<string name="chat_delete_dialog">Delete dialog</string>
|
||||
<string name="chat_leave_delete">Leave and delete chat</string>
|
||||
<string name="chat_leave_chat">Leave chat</string>
|
||||
<string name="chat_leave">Leave</string>
|
||||
<string name="chat_search_in_chat">Search in chat</string>
|
||||
<string name="chat_search_prev">Prev</string>
|
||||
<string name="chat_search_next">Next</string>
|
||||
<string name="chat_search_gifs">Search GIFs</string>
|
||||
<string name="chat_search_matches">Matches: %1$d</string>
|
||||
<string name="chat_pinned_message">Pinned message</string>
|
||||
<string name="chat_message_placeholder">Message</string>
|
||||
<string name="chat_action_reply">Reply</string>
|
||||
<string name="chat_action_edit">Edit</string>
|
||||
<string name="chat_action_forward">Forward</string>
|
||||
<string name="chat_action_delete">Delete</string>
|
||||
<string name="chat_delete_message_title">Delete message</string>
|
||||
<string name="chat_forward_one">Forward message #%1$d</string>
|
||||
<string name="chat_forward_many">Forward %1$d messages</string>
|
||||
<string name="chat_no_available_chats">No available chats</string>
|
||||
<string name="chat_forwarding">Forwarding...</string>
|
||||
<string name="chat_editing_message">Editing message #%1$d</string>
|
||||
<string name="chat_reply_to">Reply to</string>
|
||||
<string name="chat_delete_message_for_everyone">Delete selected message for everyone?</string>
|
||||
<string name="chat_delete_message_for_me">Delete selected message(s) for you?</string>
|
||||
<string name="chat_clear_history_confirm">Delete all messages in this chat? This action cannot be undone.</string>
|
||||
<string name="chat_clear">Clear</string>
|
||||
<string name="chat_delete_dialog_confirm">Delete dialog for you? History will be cleared.</string>
|
||||
<string name="chat_leave_delete_confirm">Leave the chat and remove it from your list?</string>
|
||||
<string name="chat_enable_sound">Enable sound</string>
|
||||
<string name="chat_circle_video">Circle video</string>
|
||||
|
||||
<string name="settings_user_fallback">User</string>
|
||||
<string name="settings_accounts_header">ACCOUNTS</string>
|
||||
<string name="settings_no_active_account">No active account</string>
|
||||
<string name="settings_add_account">Add account</string>
|
||||
<string name="settings_folder_account">Account</string>
|
||||
<string name="settings_folder_chat">Chat settings</string>
|
||||
<string name="settings_folder_privacy">Privacy</string>
|
||||
<string name="settings_folder_notifications">Notifications</string>
|
||||
<string name="settings_folder_data">Data and storage</string>
|
||||
<string name="settings_folder_folders">Chat folders</string>
|
||||
<string name="settings_folder_devices">Devices</string>
|
||||
<string name="settings_folder_power">Power saving</string>
|
||||
<string name="settings_folder_language">Language</string>
|
||||
<string name="settings_account_subtitle">Phone number, username, bio</string>
|
||||
<string name="settings_chat_subtitle">Wallpaper, night mode, animations</string>
|
||||
<string name="settings_privacy_subtitle">Last seen, devices, passkeys</string>
|
||||
<string name="settings_notifications_subtitle">Sounds, message counter</string>
|
||||
<string name="settings_data_subtitle">Media download settings</string>
|
||||
<string name="settings_folders_subtitle">Sort chats by folders</string>
|
||||
<string name="settings_devices_subtitle">Manage active sessions</string>
|
||||
<string name="settings_power_subtitle">Save power on low battery</string>
|
||||
<string name="settings_placeholder_data">This section will be expanded in the next step.</string>
|
||||
<string name="settings_placeholder_folders">Chat folders management will be added in next iteration.</string>
|
||||
<string name="settings_placeholder_power">Power saving settings will be added in a separate step.</string>
|
||||
<string name="settings_language_title">App language</string>
|
||||
<string name="settings_language_system_subtitle">Use device language</string>
|
||||
<string name="settings_language_english_subtitle">English</string>
|
||||
<string name="language_system">System</string>
|
||||
<string name="language_russian">Russian</string>
|
||||
<string name="language_english">English</string>
|
||||
<string name="settings_accounts">Accounts</string>
|
||||
<string name="settings_active">Active</string>
|
||||
<string name="settings_switch">Switch</string>
|
||||
<string name="settings_open_profile">Open profile</string>
|
||||
<string name="settings_logout">Logout</string>
|
||||
<string name="settings_appearance">Appearance</string>
|
||||
<string name="theme_light">Light</string>
|
||||
<string name="theme_dark">Dark</string>
|
||||
<string name="theme_system">System</string>
|
||||
<string name="settings_enable_notifications">Enable notifications</string>
|
||||
<string name="settings_show_preview">Show message preview</string>
|
||||
<string name="settings_refresh_notifications">Refresh notification history</string>
|
||||
<string name="settings_recent_notifications">Recent notifications</string>
|
||||
<string name="settings_no_notifications_yet">No server notifications yet.</string>
|
||||
<string name="privacy_private_messages">Private messages</string>
|
||||
<string name="privacy_last_seen">Last seen</string>
|
||||
<string name="privacy_avatar">Avatar</string>
|
||||
<string name="privacy_group_invites">Group invites</string>
|
||||
<string name="privacy_save">Save privacy</string>
|
||||
<string name="settings_sessions_security">Sessions & Security</string>
|
||||
<string name="settings_unknown_device">Unknown device</string>
|
||||
<string name="settings_unknown_ip">Unknown IP</string>
|
||||
<string name="settings_revoke">Revoke</string>
|
||||
<string name="settings_revoke_all">Revoke all sessions</string>
|
||||
<string name="settings_2fa_code">2FA code</string>
|
||||
<string name="settings_enable">Enable</string>
|
||||
<string name="settings_disable">Disable</string>
|
||||
<string name="settings_recovery_regen_code">Code for recovery regeneration</string>
|
||||
<string name="settings_regenerate_recovery_codes">Regenerate recovery codes</string>
|
||||
<string name="privacy_everyone">everyone</string>
|
||||
<string name="privacy_contacts">contacts</string>
|
||||
<string name="privacy_nobody">nobody</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user