mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Merge branch 'master' into master-android
This commit is contained in:
+3
-5
@@ -2,6 +2,7 @@ package chat.simplex.common.platform
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Build
|
||||
import android.text.InputType
|
||||
import android.util.Log
|
||||
@@ -16,6 +17,7 @@ import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
@@ -56,7 +58,6 @@ actual fun PlatformTextField(
|
||||
) {
|
||||
val cs = composeState.value
|
||||
val textColor = MaterialTheme.colors.onBackground
|
||||
val tintColor = MaterialTheme.colors.secondaryVariant
|
||||
val padding = PaddingValues(12.dp, 7.dp, 45.dp, 0.dp)
|
||||
val paddingStart = with(LocalDensity.current) { 12.dp.roundToPx() }
|
||||
val paddingTop = with(LocalDensity.current) { 7.dp.roundToPx() }
|
||||
@@ -109,9 +110,7 @@ actual fun PlatformTextField(
|
||||
editText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or editText.inputType
|
||||
editText.setTextColor(textColor.toArgb())
|
||||
editText.textSize = textStyle.value.fontSize.value * appPrefs.fontScale.get()
|
||||
val drawable = androidAppContext.getDrawable(R.drawable.send_msg_view_background)!!
|
||||
DrawableCompat.setTint(drawable, tintColor.toArgb())
|
||||
editText.background = drawable
|
||||
editText.background = ColorDrawable(Color.Transparent.toArgb())
|
||||
editText.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom)
|
||||
editText.setText(cs.message)
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
@@ -137,7 +136,6 @@ actual fun PlatformTextField(
|
||||
}) {
|
||||
it.setTextColor(textColor.toArgb())
|
||||
it.textSize = textStyle.value.fontSize.value * appPrefs.fontScale.get()
|
||||
DrawableCompat.setTint(it.background, tintColor.toArgb())
|
||||
it.isFocusable = composeState.value.preview !is ComposePreview.VoicePreview
|
||||
it.isFocusableInTouchMode = it.isFocusable
|
||||
if (cs.message != it.text.toString()) {
|
||||
|
||||
+12
@@ -2,12 +2,15 @@ package chat.simplex.common.views.chat.item
|
||||
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import chat.simplex.common.model.CIFile
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
import chat.simplex.common.views.helpers.ModalManager
|
||||
import coil.ImageLoader
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
@@ -21,6 +24,7 @@ actual fun SimpleAndAnimatedImageView(
|
||||
imageBitmap: ImageBitmap,
|
||||
file: CIFile?,
|
||||
imageProvider: () -> ImageGalleryProvider,
|
||||
smallView: Boolean,
|
||||
ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -35,6 +39,14 @@ actual fun SimpleAndAnimatedImageView(
|
||||
if (getLoadedFilePath(file) != null) {
|
||||
ModalManager.fullscreen.showCustomModal(animated = false) { close ->
|
||||
ImageFullScreenView(imageProvider, close)
|
||||
if (smallView) {
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
val c = CurrentColors.value.colors
|
||||
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -6,6 +6,7 @@ import androidx.compose.material.Divider
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.platform.onRightClick
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -19,8 +20,14 @@ actual fun ChatListNavLinkLayout(
|
||||
disabled: Boolean,
|
||||
selectedChat: State<Boolean>,
|
||||
nextChatSelected: State<Boolean>,
|
||||
oneHandUI: State<Boolean>
|
||||
) {
|
||||
var modifier = Modifier.fillMaxWidth()
|
||||
|
||||
if (oneHandUI != null && oneHandUI.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
if (!disabled) modifier = modifier
|
||||
.combinedClickable(onClick = click, onLongClick = { showMenu.value = true })
|
||||
.onRightClick { showMenu.value = true }
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ private val CALL_BOTTOM_ICON_OFFSET = (-15).dp
|
||||
private val CALL_BOTTOM_ICON_HEIGHT = CALL_INTERACTIVE_AREA_HEIGHT + CALL_BOTTOM_ICON_OFFSET
|
||||
|
||||
@Composable
|
||||
actual fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow<AnimatedViewState>) {
|
||||
actual fun ActiveCallInteractiveArea(call: Call) {
|
||||
val onClick = { platform.androidStartCallActivity(false) }
|
||||
Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT)) {
|
||||
val source = remember { MutableInteractionSource() }
|
||||
|
||||
+7
-1
@@ -30,6 +30,7 @@ import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.helpers.APPLICATION_ID
|
||||
import chat.simplex.common.helpers.saveAppLocale
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
@@ -78,7 +79,7 @@ fun AppearanceScope.AppearanceLayout(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.appearance_settings))
|
||||
SectionView(stringResource(MR.strings.settings_section_title_language), padding = PaddingValues()) {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_interface), padding = PaddingValues()) {
|
||||
val context = LocalContext.current
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
// SectionItemWithValue(
|
||||
@@ -104,6 +105,11 @@ fun AppearanceScope.AppearanceLayout(
|
||||
}
|
||||
}
|
||||
// }
|
||||
|
||||
SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI) {
|
||||
val c = CurrentColors.value.colors
|
||||
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, false, false)
|
||||
}
|
||||
}
|
||||
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
|
||||
@@ -235,7 +235,7 @@ fun AndroidScreen(settingsState: SettingsViewState) {
|
||||
BoxWithConstraints {
|
||||
val call = remember { chatModel.activeCall} .value
|
||||
val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted
|
||||
var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) }
|
||||
val currentChatId = remember { mutableStateOf(chatModel.chatId.value) }
|
||||
val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) }
|
||||
Box(
|
||||
Modifier
|
||||
@@ -264,21 +264,35 @@ fun AndroidScreen(settingsState: SettingsViewState) {
|
||||
snapshotFlow { chatModel.chatId.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
if (it == null) onComposed(null)
|
||||
currentChatId = it
|
||||
if (it == null) {
|
||||
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
|
||||
onComposed(null)
|
||||
}
|
||||
currentChatId.value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { ModalManager.center.modalCount.value > 0 }
|
||||
.filter { chatModel.chatId.value == null }
|
||||
.collect { modalBackground ->
|
||||
if (modalBackground && !chatModel.newChatSheetVisible.value) {
|
||||
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false)
|
||||
} else {
|
||||
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier
|
||||
.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }
|
||||
.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)
|
||||
) Box2@{
|
||||
currentChatId?.let {
|
||||
ChatView(it, chatModel, onComposed)
|
||||
currentChatId.value?.let {
|
||||
ChatView(currentChatId, onComposed)
|
||||
}
|
||||
}
|
||||
if (call != null && showCallArea) {
|
||||
ActiveCallInteractiveArea(call, remember { MutableStateFlow(AnimatedViewState.GONE) })
|
||||
ActiveCallInteractiveArea(call)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,7 +312,7 @@ fun StartPartOfScreen(settingsState: SettingsViewState) {
|
||||
|
||||
@Composable
|
||||
fun CenterPartOfScreen() {
|
||||
val currentChatId by remember { ChatModel.chatId }
|
||||
val currentChatId = remember { ChatModel.chatId }
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { currentChatId }
|
||||
.distinctUntilChanged()
|
||||
@@ -308,7 +322,7 @@ fun CenterPartOfScreen() {
|
||||
}
|
||||
}
|
||||
}
|
||||
when (val id = currentChatId) {
|
||||
when (currentChatId.value) {
|
||||
null -> {
|
||||
if (!rememberUpdatedState(ModalManager.center.hasModalsOpen()).value) {
|
||||
Box(
|
||||
@@ -323,7 +337,7 @@ fun CenterPartOfScreen() {
|
||||
ModalManager.center.showInView()
|
||||
}
|
||||
}
|
||||
else -> ChatView(id, chatModel) {}
|
||||
else -> ChatView(currentChatId) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+335
-296
@@ -54,7 +54,9 @@ object ChatModel {
|
||||
val ctrlInitInProgress = mutableStateOf(false)
|
||||
val dbMigrationInProgress = mutableStateOf(false)
|
||||
val incompleteInitializedDbRemoved = mutableStateOf(false)
|
||||
val chats = mutableStateListOf<Chat>()
|
||||
private val _chats = mutableStateOf(SnapshotStateList<Chat>())
|
||||
val chats: State<List<Chat>> = _chats
|
||||
private val chatsContext = ChatsContext()
|
||||
// map of connections network statuses, key is agent connection id
|
||||
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
|
||||
val switchingUsersAndHosts = mutableStateOf(false)
|
||||
@@ -81,6 +83,9 @@ object ChatModel {
|
||||
// set when app is opened via contact or invitation URI (rhId, uri)
|
||||
val appOpenUrl = mutableStateOf<Pair<Long?, URI>?>(null)
|
||||
|
||||
// Needed to check for bottom nav bar and to apply or not navigation bar color on Android
|
||||
val newChatSheetVisible = mutableStateOf(false)
|
||||
|
||||
// preferences
|
||||
val notificationPreviewMode by lazy {
|
||||
mutableStateOf(
|
||||
@@ -126,7 +131,7 @@ object ChatModel {
|
||||
val updatingProgress = mutableStateOf(null as Float?)
|
||||
var updatingRequest: Closeable? = null
|
||||
|
||||
val updatingChatsMutex: Mutex = Mutex()
|
||||
private val updatingChatsMutex: Mutex = Mutex()
|
||||
val changingActiveUserMutex: Mutex = Mutex()
|
||||
|
||||
val desktopNoUserNoRemote: Boolean @Composable get() = appPlatform.isDesktop && currentUser.value == null && currentRemoteHost.value == null
|
||||
@@ -170,11 +175,11 @@ object ChatModel {
|
||||
}
|
||||
|
||||
// toList() here is to prevent ConcurrentModificationException that is rarely happens but happens
|
||||
fun hasChat(rhId: Long?, id: String): Boolean = chats.toList().firstOrNull { it.id == id && it.remoteHostId == rhId } != null
|
||||
fun hasChat(rhId: Long?, id: String): Boolean = chats.value.firstOrNull { it.id == id && it.remoteHostId == rhId } != null
|
||||
// TODO pass rhId?
|
||||
fun getChat(id: String): Chat? = chats.toList().firstOrNull { it.id == id }
|
||||
fun getContactChat(contactId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId }
|
||||
fun getGroupChat(groupId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId }
|
||||
fun getChat(id: String): Chat? = chats.value.firstOrNull { it.id == id }
|
||||
fun getContactChat(contactId: Long): Chat? = chats.value.firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId }
|
||||
fun getGroupChat(groupId: Long): Chat? = chats.value.firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId }
|
||||
|
||||
fun populateGroupMembersIndexes() {
|
||||
groupMembersIndexes.clear()
|
||||
@@ -192,97 +197,102 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getChatIndex(rhId: Long?, id: String): Int = chats.toList().indexOfFirst { it.id == id && it.remoteHostId == rhId }
|
||||
fun addChat(chat: Chat) = chats.add(index = 0, chat)
|
||||
suspend fun <T> withChats(action: suspend ChatsContext.() -> T): T = updatingChatsMutex.withLock {
|
||||
chatsContext.action()
|
||||
}
|
||||
|
||||
fun updateChatInfo(rhId: Long?, cInfo: ChatInfo) {
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
if (i >= 0) {
|
||||
val currentCInfo = chats[i].chatInfo
|
||||
var newCInfo = cInfo
|
||||
if (currentCInfo is ChatInfo.Direct && newCInfo is ChatInfo.Direct) {
|
||||
val currentStats = currentCInfo.contact.activeConn?.connectionStats
|
||||
val newConn = newCInfo.contact.activeConn
|
||||
val newStats = newConn?.connectionStats
|
||||
if (currentStats != null && newConn != null && newStats == null) {
|
||||
newCInfo = newCInfo.copy(
|
||||
contact = newCInfo.contact.copy(
|
||||
activeConn = newConn.copy(
|
||||
connectionStats = currentStats
|
||||
class ChatsContext {
|
||||
val chats = _chats
|
||||
|
||||
fun addChat(chat: Chat) = chats.add(index = 0, chat)
|
||||
|
||||
fun updateChatInfo(rhId: Long?, cInfo: ChatInfo) {
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
if (i >= 0) {
|
||||
val currentCInfo = chats[i].chatInfo
|
||||
var newCInfo = cInfo
|
||||
if (currentCInfo is ChatInfo.Direct && newCInfo is ChatInfo.Direct) {
|
||||
val currentStats = currentCInfo.contact.activeConn?.connectionStats
|
||||
val newConn = newCInfo.contact.activeConn
|
||||
val newStats = newConn?.connectionStats
|
||||
if (currentStats != null && newConn != null && newStats == null) {
|
||||
newCInfo = newCInfo.copy(
|
||||
contact = newCInfo.contact.copy(
|
||||
activeConn = newConn.copy(
|
||||
connectionStats = currentStats
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
chats[i] = chats[i].copy(chatInfo = newCInfo)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateContactConnection(rhId: Long?, contactConnection: PendingContactConnection) = updateChat(rhId, ChatInfo.ContactConnection(contactConnection))
|
||||
|
||||
fun updateContact(rhId: Long?, contact: Contact) = updateChat(rhId, ChatInfo.Direct(contact), addMissing = contact.directOrUsed)
|
||||
|
||||
fun updateContactConnectionStats(rhId: Long?, contact: Contact, connectionStats: ConnectionStats) {
|
||||
val updatedConn = contact.activeConn?.copy(connectionStats = connectionStats)
|
||||
val updatedContact = contact.copy(activeConn = updatedConn)
|
||||
updateContact(rhId, updatedContact)
|
||||
}
|
||||
|
||||
fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo))
|
||||
|
||||
private fun updateChat(rhId: Long?, cInfo: ChatInfo, addMissing: Boolean = true) {
|
||||
if (hasChat(rhId, cInfo.id)) {
|
||||
updateChatInfo(rhId, cInfo)
|
||||
} else if (addMissing) {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf()))
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChats(newChats: List<Chat>) {
|
||||
chats.clear()
|
||||
chats.addAll(newChats)
|
||||
|
||||
val cId = chatId.value
|
||||
// If chat is null, it was deleted in background after apiGetChats call
|
||||
if (cId != null && getChat(cId) == null) {
|
||||
chatId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
fun replaceChat(rhId: Long?, id: String, chat: Chat) {
|
||||
val i = getChatIndex(rhId, id)
|
||||
if (i >= 0) {
|
||||
chats[i] = chat
|
||||
} else {
|
||||
// invalid state, correcting
|
||||
chats.add(index = 0, chat)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) = updatingChatsMutex.withLock {
|
||||
// update previews
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
if (i >= 0) {
|
||||
chat = chats[i]
|
||||
val newPreviewItem = when (cInfo) {
|
||||
is ChatInfo.Group -> {
|
||||
val currentPreviewItem = chat.chatItems.firstOrNull()
|
||||
if (currentPreviewItem != null) {
|
||||
if (cItem.meta.itemTs >= currentPreviewItem.meta.itemTs) {
|
||||
cItem
|
||||
} else {
|
||||
currentPreviewItem
|
||||
}
|
||||
} else {
|
||||
cItem
|
||||
}
|
||||
}
|
||||
else -> cItem
|
||||
chats[i] = chats[i].copy(chatInfo = newCInfo)
|
||||
}
|
||||
chats[i] = chat.copy(
|
||||
chatItems = arrayListOf(newPreviewItem),
|
||||
chatStats =
|
||||
}
|
||||
|
||||
fun updateContactConnection(rhId: Long?, contactConnection: PendingContactConnection) = updateChat(rhId, ChatInfo.ContactConnection(contactConnection))
|
||||
|
||||
fun updateContact(rhId: Long?, contact: Contact) = updateChat(rhId, ChatInfo.Direct(contact), addMissing = contact.directOrUsed)
|
||||
|
||||
fun updateContactConnectionStats(rhId: Long?, contact: Contact, connectionStats: ConnectionStats) {
|
||||
val updatedConn = contact.activeConn?.copy(connectionStats = connectionStats)
|
||||
val updatedContact = contact.copy(activeConn = updatedConn)
|
||||
updateContact(rhId, updatedContact)
|
||||
}
|
||||
|
||||
fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo))
|
||||
|
||||
private fun updateChat(rhId: Long?, cInfo: ChatInfo, addMissing: Boolean = true) {
|
||||
if (hasChat(rhId, cInfo.id)) {
|
||||
updateChatInfo(rhId, cInfo)
|
||||
} else if (addMissing) {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf()))
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChats(newChats: List<Chat>) {
|
||||
chats.clear()
|
||||
chats.addAll(newChats)
|
||||
|
||||
val cId = chatId.value
|
||||
// If chat is null, it was deleted in background after apiGetChats call
|
||||
if (cId != null && getChat(cId) == null) {
|
||||
chatId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
fun replaceChat(rhId: Long?, id: String, chat: Chat) {
|
||||
val i = getChatIndex(rhId, id)
|
||||
if (i >= 0) {
|
||||
chats[i] = chat
|
||||
} else {
|
||||
// invalid state, correcting
|
||||
chats.add(index = 0, chat)
|
||||
}
|
||||
}
|
||||
suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) {
|
||||
// update previews
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
if (i >= 0) {
|
||||
chat = chats[i]
|
||||
val newPreviewItem = when (cInfo) {
|
||||
is ChatInfo.Group -> {
|
||||
val currentPreviewItem = chat.chatItems.firstOrNull()
|
||||
if (currentPreviewItem != null) {
|
||||
if (cItem.meta.itemTs >= currentPreviewItem.meta.itemTs) {
|
||||
cItem
|
||||
} else {
|
||||
currentPreviewItem
|
||||
}
|
||||
} else {
|
||||
cItem
|
||||
}
|
||||
}
|
||||
else -> cItem
|
||||
}
|
||||
chats[i] = chat.copy(
|
||||
chatItems = arrayListOf(newPreviewItem),
|
||||
chatStats =
|
||||
if (cItem.meta.itemStatus is CIStatus.RcvNew) {
|
||||
val minUnreadId = if(chat.chatStats.minUnreadItemId == 0L) cItem.id else chat.chatStats.minUnreadItemId
|
||||
increaseUnreadCounter(rhId, currentUser.value!!)
|
||||
@@ -290,123 +300,197 @@ object ChatModel {
|
||||
}
|
||||
else
|
||||
chat.chatStats
|
||||
)
|
||||
if (i > 0) {
|
||||
popChat_(i)
|
||||
)
|
||||
if (i > 0) {
|
||||
chats.add(index = 0, chats.removeAt(i))
|
||||
}
|
||||
} else {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||
}
|
||||
} else {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
// add to current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
// Prevent situation when chat item already in the list received from backend
|
||||
if (chatItems.value.none { it.id == cItem.id }) {
|
||||
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||
chatItems.add(kotlin.math.max(0, chatItems.value.lastIndex), cItem)
|
||||
} else {
|
||||
chatItems.add(cItem)
|
||||
withContext(Dispatchers.Main) {
|
||||
// add to current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
// Prevent situation when chat item already in the list received from backend
|
||||
if (chatItems.value.none { it.id == cItem.id }) {
|
||||
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||
chatItems.add(kotlin.math.max(0, chatItems.value.lastIndex), cItem)
|
||||
} else {
|
||||
chatItems.add(cItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean = updatingChatsMutex.withLock {
|
||||
// update previews
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
val res: Boolean
|
||||
if (i >= 0) {
|
||||
chat = chats[i]
|
||||
val pItem = chat.chatItems.lastOrNull()
|
||||
if (pItem?.id == cItem.id) {
|
||||
chats[i] = chat.copy(chatItems = arrayListOf(cItem))
|
||||
if (pItem.isRcvNew && !cItem.isRcvNew) {
|
||||
// status changed from New to Read, update counter
|
||||
decreaseCounterInChat(rhId, cInfo.id)
|
||||
suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean {
|
||||
// update previews
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
val res: Boolean
|
||||
if (i >= 0) {
|
||||
chat = chats[i]
|
||||
val pItem = chat.chatItems.lastOrNull()
|
||||
if (pItem?.id == cItem.id) {
|
||||
chats[i] = chat.copy(chatItems = arrayListOf(cItem))
|
||||
if (pItem.isRcvNew && !cItem.isRcvNew) {
|
||||
// status changed from New to Read, update counter
|
||||
decreaseCounterInChat(rhId, cInfo.id)
|
||||
}
|
||||
}
|
||||
res = false
|
||||
} else {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||
res = true
|
||||
}
|
||||
return withContext(Dispatchers.Main) {
|
||||
// update current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
val items = chatItems.value
|
||||
val itemIndex = items.indexOfFirst { it.id == cItem.id }
|
||||
if (itemIndex >= 0) {
|
||||
items[itemIndex] = cItem
|
||||
false
|
||||
} else {
|
||||
val status = chatItemStatuses.remove(cItem.id)
|
||||
val ci = if (status != null && cItem.meta.itemStatus is CIStatus.SndNew) {
|
||||
cItem.copy(meta = cItem.meta.copy(itemStatus = status))
|
||||
} else {
|
||||
cItem
|
||||
}
|
||||
chatItems.add(ci)
|
||||
true
|
||||
}
|
||||
} else {
|
||||
res
|
||||
}
|
||||
}
|
||||
res = false
|
||||
} else {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||
res = true
|
||||
}
|
||||
return withContext(Dispatchers.Main) {
|
||||
// update current chat
|
||||
|
||||
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (chatId.value == cInfo.id) {
|
||||
val items = chatItems.value
|
||||
val itemIndex = items.indexOfFirst { it.id == cItem.id }
|
||||
if (itemIndex >= 0) {
|
||||
items[itemIndex] = cItem
|
||||
}
|
||||
} else if (status != null) {
|
||||
chatItemStatuses[cItem.id] = status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) {
|
||||
if (cItem.isRcvNew) {
|
||||
decreaseCounterInChat(rhId, cInfo.id)
|
||||
}
|
||||
// update previews
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
if (i >= 0) {
|
||||
chat = chats[i]
|
||||
val pItem = chat.chatItems.lastOrNull()
|
||||
if (pItem?.id == cItem.id) {
|
||||
chats[i] = chat.copy(chatItems = arrayListOf(ChatItem.deletedItemDummy))
|
||||
}
|
||||
}
|
||||
// remove from current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
val items = chatItems.value
|
||||
val itemIndex = items.indexOfFirst { it.id == cItem.id }
|
||||
if (itemIndex >= 0) {
|
||||
items[itemIndex] = cItem
|
||||
chatItems.removeAll {
|
||||
val remove = it.id == cItem.id
|
||||
if (remove) { AudioPlayer.stop(it) }
|
||||
remove
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearChat(rhId: Long?, cInfo: ChatInfo) {
|
||||
// clear preview
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
if (i >= 0) {
|
||||
decreaseUnreadCounter(rhId, currentUser.value!!, chats[i].chatStats.unreadCount)
|
||||
chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo)
|
||||
}
|
||||
// clear current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
chatItemStatuses.clear()
|
||||
chatItems.clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun markChatItemsRead(remoteHostId: Long?, chatInfo: ChatInfo, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) {
|
||||
val cInfo = chatInfo
|
||||
val markedRead = markItemsReadInCurrentChat(chatInfo, range)
|
||||
// update preview
|
||||
val chatIdx = getChatIndex(remoteHostId, cInfo.id)
|
||||
if (chatIdx >= 0) {
|
||||
val chat = chats[chatIdx]
|
||||
val lastId = chat.chatItems.lastOrNull()?.id
|
||||
if (lastId != null) {
|
||||
val unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0
|
||||
decreaseUnreadCounter(remoteHostId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||
chats[chatIdx] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(
|
||||
unreadCount = unreadCount,
|
||||
// Can't use minUnreadItemId currently since chat items can have unread items between read items
|
||||
//minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decreaseCounterInChat(rhId: Long?, chatId: ChatId) {
|
||||
val chatIndex = getChatIndex(rhId, chatId)
|
||||
if (chatIndex == -1) return
|
||||
|
||||
val chat = chats[chatIndex]
|
||||
val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0)
|
||||
decreaseUnreadCounter(rhId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||
chats[chatIndex] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(
|
||||
unreadCount = unreadCount,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun removeChat(rhId: Long?, id: String) {
|
||||
chats.removeAll { it.id == id && it.remoteHostId == rhId }
|
||||
}
|
||||
|
||||
fun upsertGroupMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember): Boolean {
|
||||
// user member was updated
|
||||
if (groupInfo.membership.groupMemberId == member.groupMemberId) {
|
||||
updateGroup(rhId, groupInfo)
|
||||
return false
|
||||
}
|
||||
// update current chat
|
||||
return if (chatId.value == groupInfo.id) {
|
||||
val memberIndex = groupMembersIndexes[member.groupMemberId]
|
||||
if (memberIndex != null) {
|
||||
groupMembers[memberIndex] = member
|
||||
false
|
||||
} else {
|
||||
val status = chatItemStatuses.remove(cItem.id)
|
||||
val ci = if (status != null && cItem.meta.itemStatus is CIStatus.SndNew) {
|
||||
cItem.copy(meta = cItem.meta.copy(itemStatus = status))
|
||||
} else {
|
||||
cItem
|
||||
}
|
||||
chatItems.add(ci)
|
||||
groupMembers.add(member)
|
||||
groupMembersIndexes[member.groupMemberId] = groupMembers.size - 1
|
||||
true
|
||||
}
|
||||
} else {
|
||||
res
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun updateGroupMemberConnectionStats(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) {
|
||||
val memberConn = member.activeConn
|
||||
if (memberConn != null) {
|
||||
val updatedConn = memberConn.copy(connectionStats = connectionStats)
|
||||
val updatedMember = member.copy(activeConn = updatedConn)
|
||||
upsertGroupMember(rhId, groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (chatId.value == cInfo.id) {
|
||||
val items = chatItems.value
|
||||
val itemIndex = items.indexOfFirst { it.id == cItem.id }
|
||||
if (itemIndex >= 0) {
|
||||
items[itemIndex] = cItem
|
||||
}
|
||||
} else if (status != null) {
|
||||
chatItemStatuses[cItem.id] = status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) {
|
||||
if (cItem.isRcvNew) {
|
||||
decreaseCounterInChat(rhId, cInfo.id)
|
||||
}
|
||||
// update previews
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
if (i >= 0) {
|
||||
chat = chats[i]
|
||||
val pItem = chat.chatItems.lastOrNull()
|
||||
if (pItem?.id == cItem.id) {
|
||||
chats[i] = chat.copy(chatItems = arrayListOf(ChatItem.deletedItemDummy))
|
||||
}
|
||||
}
|
||||
// remove from current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
chatItems.removeAll {
|
||||
val remove = it.id == cItem.id
|
||||
if (remove) { AudioPlayer.stop(it) }
|
||||
remove
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearChat(rhId: Long?, cInfo: ChatInfo) {
|
||||
// clear preview
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
if (i >= 0) {
|
||||
decreaseUnreadCounter(rhId, currentUser.value!!, chats[i].chatStats.unreadCount)
|
||||
chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo)
|
||||
}
|
||||
// clear current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
chatItemStatuses.clear()
|
||||
chatItems.clear()
|
||||
}
|
||||
}
|
||||
private fun getChatIndex(rhId: Long?, id: String): Int = chats.value.indexOfFirst { it.id == id && it.remoteHostId == rhId }
|
||||
|
||||
fun updateCurrentUser(rhId: Long?, newProfile: Profile, preferences: FullChatPreferences? = null) {
|
||||
val current = currentUser.value ?: return
|
||||
@@ -447,30 +531,8 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
fun markChatItemsRead(chat: Chat, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) {
|
||||
val cInfo = chat.chatInfo
|
||||
val markedRead = markItemsReadInCurrentChat(chat, range)
|
||||
// update preview
|
||||
val chatIdx = getChatIndex(chat.remoteHostId, cInfo.id)
|
||||
if (chatIdx >= 0) {
|
||||
val chat = chats[chatIdx]
|
||||
val lastId = chat.chatItems.lastOrNull()?.id
|
||||
if (lastId != null) {
|
||||
val unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0
|
||||
decreaseUnreadCounter(chat.remoteHostId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||
chats[chatIdx] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(
|
||||
unreadCount = unreadCount,
|
||||
// Can't use minUnreadItemId currently since chat items can have unread items between read items
|
||||
//minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun markItemsReadInCurrentChat(chat: Chat, range: CC.ItemRange? = null): Int {
|
||||
val cInfo = chat.chatInfo
|
||||
private fun markItemsReadInCurrentChat(chatInfo: ChatInfo, range: CC.ItemRange? = null): Int {
|
||||
val cInfo = chatInfo
|
||||
var markedRead = 0
|
||||
if (chatId.value == cInfo.id) {
|
||||
var i = 0
|
||||
@@ -493,20 +555,6 @@ object ChatModel {
|
||||
return markedRead
|
||||
}
|
||||
|
||||
private fun decreaseCounterInChat(rhId: Long?, chatId: ChatId) {
|
||||
val chatIndex = getChatIndex(rhId, chatId)
|
||||
if (chatIndex == -1) return
|
||||
|
||||
val chat = chats[chatIndex]
|
||||
val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0)
|
||||
decreaseUnreadCounter(rhId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||
chats[chatIndex] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(
|
||||
unreadCount = unreadCount,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun increaseUnreadCounter(rhId: Long?, user: UserLike) {
|
||||
changeUnreadCounter(rhId, user, 1)
|
||||
}
|
||||
@@ -600,11 +648,6 @@ object ChatModel {
|
||||
// }
|
||||
// }
|
||||
|
||||
private fun popChat_(i: Int) {
|
||||
val chat = chats.removeAt(i)
|
||||
chats.add(index = 0, chat)
|
||||
}
|
||||
|
||||
fun replaceConnReqView(id: String, withId: String) {
|
||||
if (id == showingInvitation.value?.connId) {
|
||||
showingInvitation.value = null
|
||||
@@ -629,41 +672,6 @@ object ChatModel {
|
||||
showingInvitation.value = showingInvitation.value?.copy(connChatUsed = true)
|
||||
}
|
||||
|
||||
fun removeChat(rhId: Long?, id: String) {
|
||||
chats.removeAll { it.id == id && it.remoteHostId == rhId }
|
||||
}
|
||||
|
||||
fun upsertGroupMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember): Boolean {
|
||||
// user member was updated
|
||||
if (groupInfo.membership.groupMemberId == member.groupMemberId) {
|
||||
updateGroup(rhId, groupInfo)
|
||||
return false
|
||||
}
|
||||
// update current chat
|
||||
return if (chatId.value == groupInfo.id) {
|
||||
val memberIndex = groupMembersIndexes[member.groupMemberId]
|
||||
if (memberIndex != null) {
|
||||
groupMembers[memberIndex] = member
|
||||
false
|
||||
} else {
|
||||
groupMembers.add(member)
|
||||
groupMembersIndexes[member.groupMemberId] = groupMembers.size - 1
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun updateGroupMemberConnectionStats(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) {
|
||||
val memberConn = member.activeConn
|
||||
if (memberConn != null) {
|
||||
val updatedConn = memberConn.copy(connectionStats = connectionStats)
|
||||
val updatedMember = member.copy(activeConn = updatedConn)
|
||||
upsertGroupMember(rhId, groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
|
||||
fun setContactNetworkStatus(contact: Contact, status: NetworkStatus) {
|
||||
val conn = contact.activeConn
|
||||
if (conn != null) {
|
||||
@@ -802,6 +810,7 @@ interface SomeChat {
|
||||
val id: ChatId
|
||||
val apiId: Long
|
||||
val ready: Boolean
|
||||
val chatDeleted: Boolean
|
||||
val sendMsgEnabled: Boolean
|
||||
val ntfsEnabled: Boolean
|
||||
val incognito: Boolean
|
||||
@@ -818,14 +827,6 @@ data class Chat(
|
||||
val chatItems: List<ChatItem>,
|
||||
val chatStats: ChatStats = ChatStats()
|
||||
) {
|
||||
val userCanSend: Boolean
|
||||
get() = when (chatInfo) {
|
||||
is ChatInfo.Direct -> true
|
||||
is ChatInfo.Group -> chatInfo.groupInfo.membership.memberRole >= GroupMemberRole.Member
|
||||
is ChatInfo.Local -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
val nextSendGrpInv: Boolean
|
||||
get() = when (chatInfo) {
|
||||
is ChatInfo.Direct -> chatInfo.contact.nextSendGrpInv
|
||||
@@ -882,6 +883,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = contact.id
|
||||
override val apiId get() = contact.apiId
|
||||
override val ready get() = contact.ready
|
||||
override val chatDeleted get() = contact.chatDeleted
|
||||
override val sendMsgEnabled get() = contact.sendMsgEnabled
|
||||
override val ntfsEnabled get() = contact.ntfsEnabled
|
||||
override val incognito get() = contact.incognito
|
||||
@@ -906,6 +908,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = groupInfo.id
|
||||
override val apiId get() = groupInfo.apiId
|
||||
override val ready get() = groupInfo.ready
|
||||
override val chatDeleted get() = groupInfo.chatDeleted
|
||||
override val sendMsgEnabled get() = groupInfo.sendMsgEnabled
|
||||
override val ntfsEnabled get() = groupInfo.ntfsEnabled
|
||||
override val incognito get() = groupInfo.incognito
|
||||
@@ -930,6 +933,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = noteFolder.id
|
||||
override val apiId get() = noteFolder.apiId
|
||||
override val ready get() = noteFolder.ready
|
||||
override val chatDeleted get() = noteFolder.chatDeleted
|
||||
override val sendMsgEnabled get() = noteFolder.sendMsgEnabled
|
||||
override val ntfsEnabled get() = noteFolder.ntfsEnabled
|
||||
override val incognito get() = noteFolder.incognito
|
||||
@@ -954,6 +958,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = contactRequest.id
|
||||
override val apiId get() = contactRequest.apiId
|
||||
override val ready get() = contactRequest.ready
|
||||
override val chatDeleted get() = contactRequest.chatDeleted
|
||||
override val sendMsgEnabled get() = contactRequest.sendMsgEnabled
|
||||
override val ntfsEnabled get() = contactRequest.ntfsEnabled
|
||||
override val incognito get() = contactRequest.incognito
|
||||
@@ -978,6 +983,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = contactConnection.id
|
||||
override val apiId get() = contactConnection.apiId
|
||||
override val ready get() = contactConnection.ready
|
||||
override val chatDeleted get() = contactConnection.chatDeleted
|
||||
override val sendMsgEnabled get() = contactConnection.sendMsgEnabled
|
||||
override val ntfsEnabled get() = false
|
||||
override val incognito get() = contactConnection.incognito
|
||||
@@ -1003,6 +1009,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = ""
|
||||
override val apiId get() = 0L
|
||||
override val ready get() = false
|
||||
override val chatDeleted get() = false
|
||||
override val sendMsgEnabled get() = false
|
||||
override val ntfsEnabled get() = false
|
||||
override val incognito get() = false
|
||||
@@ -1036,6 +1043,15 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
is ContactConnection -> contactConnection.updatedAt
|
||||
is InvalidJSON -> updatedAt
|
||||
}
|
||||
|
||||
val userCanSend: Boolean
|
||||
get() = when (this) {
|
||||
is ChatInfo.Direct -> true
|
||||
is ChatInfo.Group -> groupInfo.membership.memberRole >= GroupMemberRole.Member
|
||||
is ChatInfo.Local -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -1079,6 +1095,7 @@ data class Contact(
|
||||
val chatTs: Instant?,
|
||||
val contactGroupMemberId: Long? = null,
|
||||
val contactGrpInvSent: Boolean,
|
||||
override val chatDeleted: Boolean,
|
||||
val uiThemes: ThemeModeOverrides? = null,
|
||||
): SomeChat, NamedChat {
|
||||
override val chatType get() = ChatType.Direct
|
||||
@@ -1153,6 +1170,7 @@ data class Contact(
|
||||
updatedAt = Clock.System.now(),
|
||||
chatTs = Clock.System.now(),
|
||||
contactGrpInvSent = false,
|
||||
chatDeleted = false,
|
||||
uiThemes = null,
|
||||
)
|
||||
}
|
||||
@@ -1161,7 +1179,8 @@ data class Contact(
|
||||
@Serializable
|
||||
enum class ContactStatus {
|
||||
@SerialName("active") Active,
|
||||
@SerialName("deleted") Deleted;
|
||||
@SerialName("deleted") Deleted,
|
||||
@SerialName("deletedByUser") DeletedByUser;
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -1304,6 +1323,7 @@ data class GroupInfo (
|
||||
override val id get() = "#$groupId"
|
||||
override val apiId get() = groupId
|
||||
override val ready get() = membership.memberActive
|
||||
override val chatDeleted get() = false
|
||||
override val sendMsgEnabled get() = membership.memberActive
|
||||
override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All
|
||||
override val incognito get() = membership.memberIncognito
|
||||
@@ -1609,6 +1629,7 @@ class NoteFolder(
|
||||
override val chatType get() = ChatType.Local
|
||||
override val id get() = "*$noteFolderId"
|
||||
override val apiId get() = noteFolderId
|
||||
override val chatDeleted get() = false
|
||||
override val ready get() = true
|
||||
override val sendMsgEnabled get() = true
|
||||
override val ntfsEnabled get() = false
|
||||
@@ -1645,6 +1666,7 @@ class UserContactRequest (
|
||||
override val chatType get() = ChatType.ContactRequest
|
||||
override val id get() = "<@$contactRequestId"
|
||||
override val apiId get() = contactRequestId
|
||||
override val chatDeleted get() = false
|
||||
override val ready get() = true
|
||||
override val sendMsgEnabled get() = false
|
||||
override val ntfsEnabled get() = false
|
||||
@@ -1684,6 +1706,7 @@ class PendingContactConnection(
|
||||
override val chatType get() = ChatType.ContactConnection
|
||||
override val id get () = ":$pccConnId"
|
||||
override val apiId get() = pccConnId
|
||||
override val chatDeleted get() = false
|
||||
override val ready get() = false
|
||||
override val sendMsgEnabled get() = false
|
||||
override val ntfsEnabled get() = false
|
||||
@@ -1760,6 +1783,12 @@ enum class ConnStatus {
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ChatItemDeletion (
|
||||
val deletedChatItem: AChatItem,
|
||||
val toChatItem: AChatItem? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class AChatItem (
|
||||
val chatInfo: ChatInfo,
|
||||
@@ -2102,45 +2131,55 @@ data class ChatItem (
|
||||
}
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.add(index: Int, chatItem: ChatItem) {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(index, chatItem) }
|
||||
fun <T> MutableState<SnapshotStateList<T>>.add(index: Int, elem: T) {
|
||||
value = SnapshotStateList<T>().apply { addAll(value); add(index, elem) }
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.add(chatItem: ChatItem) {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(chatItem) }
|
||||
fun <T> MutableState<SnapshotStateList<T>>.add(elem: T) {
|
||||
value = SnapshotStateList<T>().apply { addAll(value); add(elem) }
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.addAll(index: Int, chatItems: List<ChatItem>) {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); addAll(index, chatItems) }
|
||||
fun <T> MutableState<SnapshotStateList<T>>.addAll(index: Int, elems: List<T>) {
|
||||
value = SnapshotStateList<T>().apply { addAll(value); addAll(index, elems) }
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.addAll(chatItems: List<ChatItem>) {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); addAll(chatItems) }
|
||||
fun <T> MutableState<SnapshotStateList<T>>.addAll(elems: List<T>) {
|
||||
value = SnapshotStateList<T>().apply { addAll(value); addAll(elems) }
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.removeAll(block: (ChatItem) -> Boolean) {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeAll(block) }
|
||||
fun <T> MutableState<SnapshotStateList<T>>.removeAll(block: (T) -> Boolean) {
|
||||
value = SnapshotStateList<T>().apply { addAll(value); removeAll(block) }
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.removeAt(index: Int) {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeAt(index) }
|
||||
fun <T> MutableState<SnapshotStateList<T>>.removeAt(index: Int): T {
|
||||
val new = SnapshotStateList<T>()
|
||||
new.addAll(value)
|
||||
val res = new.removeAt(index)
|
||||
value = new
|
||||
return res
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.removeLast() {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeLast() }
|
||||
fun <T> MutableState<SnapshotStateList<T>>.removeLast() {
|
||||
value = SnapshotStateList<T>().apply { addAll(value); removeLast() }
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.replaceAll(chatItems: List<ChatItem>) {
|
||||
value = SnapshotStateList<ChatItem>().apply { addAll(chatItems) }
|
||||
fun <T> MutableState<SnapshotStateList<T>>.replaceAll(elems: List<T>) {
|
||||
value = SnapshotStateList<T>().apply { addAll(elems) }
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<ChatItem>>.clear() {
|
||||
value = SnapshotStateList<ChatItem>()
|
||||
fun <T> MutableState<SnapshotStateList<T>>.clear() {
|
||||
value = SnapshotStateList<T>()
|
||||
}
|
||||
|
||||
fun State<SnapshotStateList<ChatItem>>.asReversed(): MutableList<ChatItem> = value.asReversed()
|
||||
fun <T> State<SnapshotStateList<T>>.asReversed(): MutableList<T> = value.asReversed()
|
||||
|
||||
val State<List<ChatItem>>.size: Int get() = value.size
|
||||
fun <T> State<SnapshotStateList<T>>.toList(): List<T> = value.toList()
|
||||
|
||||
operator fun <T> State<SnapshotStateList<T>>.get(i: Int): T = value[i]
|
||||
|
||||
operator fun <T> State<SnapshotStateList<T>>.set(index: Int, elem: T) { value[index] = elem }
|
||||
|
||||
val State<List<Any>>.size: Int get() = value.size
|
||||
|
||||
enum class CIMergeCategory {
|
||||
MemberConnected,
|
||||
|
||||
+254
-123
@@ -15,8 +15,8 @@ import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import chat.simplex.common.model.ChatController.getNetCfg
|
||||
import chat.simplex.common.model.ChatController.setNetCfg
|
||||
import chat.simplex.common.model.ChatModel.updatingChatsMutex
|
||||
import chat.simplex.common.model.ChatModel.changingActiveUserMutex
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
@@ -217,13 +217,16 @@ class AppPreferences {
|
||||
|
||||
val desktopWindowState = mkStrPreference(SHARED_PREFS_DESKTOP_WINDOW_STATE, null)
|
||||
|
||||
val showDeleteConversationNotice = mkBoolPreference(SHARED_PREFS_SHOW_DELETE_CONVERSATION_NOTICE, true)
|
||||
val showDeleteContactNotice = mkBoolPreference(SHARED_PREFS_SHOW_DELETE_CONTACT_NOTICE, true)
|
||||
val showSentViaProxy = mkBoolPreference(SHARED_PREFS_SHOW_SENT_VIA_RPOXY, false)
|
||||
|
||||
|
||||
val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true)
|
||||
val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false)
|
||||
|
||||
|
||||
val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, false)
|
||||
|
||||
private fun mkIntPreference(prefName: String, default: Int) =
|
||||
SharedPreference(
|
||||
get = fun() = settings.getInt(prefName, default),
|
||||
@@ -381,6 +384,7 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt"
|
||||
private const val SHARED_PREFS_NEW_DATABASE_INITIALIZED = "NewDatabaseInitialized"
|
||||
private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades"
|
||||
private const val SHARED_PREFS_ONE_HAND_UI = "OneHandUI"
|
||||
private const val SHARED_PREFS_SELF_DESTRUCT = "LocalAuthenticationSelfDestruct"
|
||||
private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName"
|
||||
private const val SHARED_PREFS_PQ_EXPERIMENTAL_ENABLED = "PQExperimentalEnabled" // no longer used
|
||||
@@ -401,6 +405,8 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto"
|
||||
private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast"
|
||||
private const val SHARED_PREFS_DESKTOP_WINDOW_STATE = "DesktopWindowState"
|
||||
private const val SHARED_PREFS_SHOW_DELETE_CONVERSATION_NOTICE = "showDeleteConversationNotice"
|
||||
private const val SHARED_PREFS_SHOW_DELETE_CONTACT_NOTICE = "showDeleteContactNotice"
|
||||
private const val SHARED_PREFS_SHOW_SENT_VIA_RPOXY = "showSentViaProxy"
|
||||
|
||||
private const val SHARED_PREFS_IOS_CALL_KIT_ENABLED = "iOSCallKitEnabled"
|
||||
@@ -478,9 +484,9 @@ object ChatController {
|
||||
}
|
||||
Log.d(TAG, "startChat: started")
|
||||
} else {
|
||||
updatingChatsMutex.withLock {
|
||||
withChats {
|
||||
val chats = apiGetChats(null)
|
||||
chatModel.updateChats(chats)
|
||||
updateChats(chats)
|
||||
}
|
||||
Log.d(TAG, "startChat: running")
|
||||
}
|
||||
@@ -558,9 +564,9 @@ object ChatController {
|
||||
val hasUser = chatModel.currentUser.value != null
|
||||
chatModel.userAddress.value = if (hasUser) apiGetUserAddress(rhId) else null
|
||||
chatModel.chatItemTTL.value = if (hasUser) getChatItemTTL(rhId) else ChatItemTTL.None
|
||||
updatingChatsMutex.withLock {
|
||||
withChats {
|
||||
val chats = apiGetChats(rhId)
|
||||
chatModel.updateChats(chats)
|
||||
updateChats(chats)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -776,9 +782,9 @@ object ChatController {
|
||||
throw Exception("failed to get app settings: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiExportArchive(config: ArchiveConfig) {
|
||||
suspend fun apiExportArchive(config: ArchiveConfig): List<ArchiveError> {
|
||||
val r = sendCmd(null, CC.ApiExportArchive(config))
|
||||
if (r is CR.CmdOk) return
|
||||
if (r is CR.ArchiveExported) return r.archiveErrors
|
||||
throw Exception("failed to export archive: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
@@ -885,16 +891,16 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiDeleteChatItem(rh: Long?, type: ChatType, id: Long, itemId: Long, mode: CIDeleteMode): CR.ChatItemDeleted? {
|
||||
val r = sendCmd(rh, CC.ApiDeleteChatItem(type, id, itemId, mode))
|
||||
if (r is CR.ChatItemDeleted) return r
|
||||
suspend fun apiDeleteChatItems(rh: Long?, type: ChatType, id: Long, itemIds: List<Long>, mode: CIDeleteMode): List<ChatItemDeletion>? {
|
||||
val r = sendCmd(rh, CC.ApiDeleteChatItem(type, id, itemIds, mode))
|
||||
if (r is CR.ChatItemsDeleted) return r.chatItemDeletions
|
||||
Log.e(TAG, "apiDeleteChatItem bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiDeleteMemberChatItem(rh: Long?, groupId: Long, groupMemberId: Long, itemId: Long): Pair<ChatItem, ChatItem?>? {
|
||||
val r = sendCmd(rh, CC.ApiDeleteMemberChatItem(groupId, groupMemberId, itemId))
|
||||
if (r is CR.ChatItemDeleted) return r.deletedChatItem.chatItem to r.toChatItem?.chatItem
|
||||
suspend fun apiDeleteMemberChatItems(rh: Long?, groupId: Long, itemIds: List<Long>): List<ChatItemDeletion>? {
|
||||
val r = sendCmd(rh, CC.ApiDeleteMemberChatItem(groupId, itemIds))
|
||||
if (r is CR.ChatItemsDeleted) return r.chatItemDeletions
|
||||
Log.e(TAG, "apiDeleteMemberChatItem bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
@@ -1177,16 +1183,18 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteChat(chat: Chat, notify: Boolean? = null) {
|
||||
suspend fun deleteChat(chat: Chat, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) {
|
||||
val cInfo = chat.chatInfo
|
||||
if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, notify = notify)) {
|
||||
chatModel.removeChat(chat.remoteHostId, cInfo.id)
|
||||
if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, chatDeleteMode = chatDeleteMode)) {
|
||||
withChats {
|
||||
removeChat(chat.remoteHostId, cInfo.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, notify: Boolean? = null): Boolean {
|
||||
suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)): Boolean {
|
||||
chatModel.deletedChats.value += rh to type.type + id
|
||||
val r = sendCmd(rh, CC.ApiDeleteChat(type, id, notify))
|
||||
val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode))
|
||||
val success = when {
|
||||
r is CR.ContactDeleted && type == ChatType.Direct -> true
|
||||
r is CR.ContactConnectionDeleted && type == ChatType.ContactConnection -> true
|
||||
@@ -1207,11 +1215,29 @@ object ChatController {
|
||||
return success
|
||||
}
|
||||
|
||||
suspend fun apiDeleteContact(rh: Long?, id: Long, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)): Contact? {
|
||||
val type = ChatType.Direct
|
||||
chatModel.deletedChats.value += rh to type.type + id
|
||||
val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode))
|
||||
val contact = when {
|
||||
r is CR.ContactDeleted -> r.contact
|
||||
else -> {
|
||||
val titleId = MR.strings.error_deleting_contact
|
||||
apiErrorAlert("apiDeleteChat", generalGetString(titleId), r)
|
||||
null
|
||||
}
|
||||
}
|
||||
chatModel.deletedChats.value -= rh to type.type + id
|
||||
return contact
|
||||
}
|
||||
|
||||
fun clearChat(chat: Chat, close: (() -> Unit)? = null) {
|
||||
withBGApi {
|
||||
val updatedChatInfo = apiClearChat(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId)
|
||||
if (updatedChatInfo != null) {
|
||||
chatModel.clearChat(chat.remoteHostId, updatedChatInfo)
|
||||
withChats {
|
||||
clearChat(chat.remoteHostId, updatedChatInfo)
|
||||
}
|
||||
ntfManager.cancelNotificationsForChat(chat.chatInfo.id)
|
||||
close?.invoke()
|
||||
}
|
||||
@@ -1546,10 +1572,12 @@ object ChatController {
|
||||
val r = sendCmd(rh, CC.ApiJoinGroup(groupId))
|
||||
when (r) {
|
||||
is CR.UserAcceptedGroupSent ->
|
||||
chatModel.updateGroup(rh, r.groupInfo)
|
||||
withChats {
|
||||
updateGroup(rh, r.groupInfo)
|
||||
}
|
||||
is CR.ChatCmdError -> {
|
||||
val e = r.chatError
|
||||
suspend fun deleteGroup() { if (apiDeleteChat(rh, ChatType.Group, groupId)) { chatModel.removeChat(rh, "#$groupId") } }
|
||||
suspend fun deleteGroup() { if (apiDeleteChat(rh, ChatType.Group, groupId)) { withChats { removeChat(rh, "#$groupId") } } }
|
||||
if (e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.AUTH) {
|
||||
deleteGroup()
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.alert_title_group_invitation_expired), generalGetString(MR.strings.alert_message_group_invitation_expired))
|
||||
@@ -1703,7 +1731,9 @@ object ChatController {
|
||||
val prefs = contact.mergedPreferences.toPreferences().setAllowed(feature, param = param)
|
||||
val toContact = apiSetContactPrefs(rh, contact.contactId, prefs)
|
||||
if (toContact != null) {
|
||||
chatModel.updateContact(rh, toContact)
|
||||
withChats {
|
||||
updateContact(rh, toContact)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1973,16 +2003,20 @@ object ChatController {
|
||||
when (r) {
|
||||
is CR.ContactDeletedByContact -> {
|
||||
if (active(r.user) && r.contact.directOrUsed) {
|
||||
chatModel.updateContact(rhId, r.contact)
|
||||
withChats {
|
||||
updateContact(rhId, r.contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ContactConnected -> {
|
||||
if (active(r.user) && r.contact.directOrUsed) {
|
||||
chatModel.updateContact(rhId, r.contact)
|
||||
val conn = r.contact.activeConn
|
||||
if (conn != null) {
|
||||
chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}")
|
||||
chatModel.removeChat(rhId, conn.id)
|
||||
withChats {
|
||||
updateContact(rhId, r.contact)
|
||||
val conn = r.contact.activeConn
|
||||
if (conn != null) {
|
||||
chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}")
|
||||
removeChat(rhId, conn.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (r.contact.directOrUsed) {
|
||||
@@ -1992,21 +2026,25 @@ object ChatController {
|
||||
}
|
||||
is CR.ContactConnecting -> {
|
||||
if (active(r.user) && r.contact.directOrUsed) {
|
||||
chatModel.updateContact(rhId, r.contact)
|
||||
val conn = r.contact.activeConn
|
||||
if (conn != null) {
|
||||
chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}")
|
||||
chatModel.removeChat(rhId, conn.id)
|
||||
withChats {
|
||||
updateContact(rhId, r.contact)
|
||||
val conn = r.contact.activeConn
|
||||
if (conn != null) {
|
||||
chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}")
|
||||
removeChat(rhId, conn.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ContactSndReady -> {
|
||||
if (active(r.user) && r.contact.directOrUsed) {
|
||||
chatModel.updateContact(rhId, r.contact)
|
||||
val conn = r.contact.activeConn
|
||||
if (conn != null) {
|
||||
chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}")
|
||||
chatModel.removeChat(rhId, conn.id)
|
||||
withChats {
|
||||
updateContact(rhId, r.contact)
|
||||
val conn = r.contact.activeConn
|
||||
if (conn != null) {
|
||||
chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}")
|
||||
removeChat(rhId, conn.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
chatModel.setContactNetworkStatus(r.contact, NetworkStatus.Connected())
|
||||
@@ -2015,10 +2053,12 @@ object ChatController {
|
||||
val contactRequest = r.contactRequest
|
||||
val cInfo = ChatInfo.ContactRequest(contactRequest)
|
||||
if (active(r.user)) {
|
||||
if (chatModel.hasChat(rhId, contactRequest.id)) {
|
||||
chatModel.updateChatInfo(rhId, cInfo)
|
||||
} else {
|
||||
chatModel.addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf()))
|
||||
withChats {
|
||||
if (chatModel.hasChat(rhId, contactRequest.id)) {
|
||||
updateChatInfo(rhId, cInfo)
|
||||
} else {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf()))
|
||||
}
|
||||
}
|
||||
}
|
||||
ntfManager.notifyContactRequestReceived(r.user, cInfo)
|
||||
@@ -2026,12 +2066,16 @@ object ChatController {
|
||||
is CR.ContactUpdated -> {
|
||||
if (active(r.user) && chatModel.hasChat(rhId, r.toContact.id)) {
|
||||
val cInfo = ChatInfo.Direct(r.toContact)
|
||||
chatModel.updateChatInfo(rhId, cInfo)
|
||||
withChats {
|
||||
updateChatInfo(rhId, cInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.GroupMemberUpdated -> {
|
||||
if (active(r.user)) {
|
||||
chatModel.upsertGroupMember(rhId, r.groupInfo, r.toMember)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.toMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ContactsMerged -> {
|
||||
@@ -2039,7 +2083,9 @@ object ChatController {
|
||||
if (chatModel.chatId.value == r.mergedContact.id) {
|
||||
chatModel.chatId.value = r.intoContact.id
|
||||
}
|
||||
chatModel.removeChat(rhId, r.mergedContact.id)
|
||||
withChats {
|
||||
removeChat(rhId, r.mergedContact.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
// ContactsSubscribed, ContactsDisconnected and ContactSubSummary are only used in CLI,
|
||||
@@ -2049,7 +2095,9 @@ object ChatController {
|
||||
is CR.ContactSubSummary -> {
|
||||
for (sub in r.contactSubscriptions) {
|
||||
if (active(r.user)) {
|
||||
chatModel.updateContact(rhId, sub.contact)
|
||||
withChats {
|
||||
updateContact(rhId, sub.contact)
|
||||
}
|
||||
}
|
||||
val err = sub.contactError
|
||||
if (err == null) {
|
||||
@@ -2073,7 +2121,15 @@ object ChatController {
|
||||
val cInfo = r.chatItem.chatInfo
|
||||
val cItem = r.chatItem.chatItem
|
||||
if (active(r.user)) {
|
||||
chatModel.addChatItem(rhId, cInfo, cItem)
|
||||
if (cInfo is ChatInfo.Direct && cInfo.chatDeleted) {
|
||||
val updatedContact = cInfo.contact.copy(chatDeleted = false)
|
||||
withChats {
|
||||
updateContact(rhId, updatedContact)
|
||||
}
|
||||
}
|
||||
withChats {
|
||||
addChatItem(rhId, cInfo, cItem)
|
||||
}
|
||||
} else if (cItem.isRcvNew && cInfo.ntfsEnabled) {
|
||||
chatModel.increaseUnreadCounter(rhId, r.user)
|
||||
}
|
||||
@@ -2094,112 +2150,150 @@ object ChatController {
|
||||
val cInfo = r.chatItem.chatInfo
|
||||
val cItem = r.chatItem.chatItem
|
||||
if (!cItem.isDeletedContent && active(r.user)) {
|
||||
chatModel.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
|
||||
withChats {
|
||||
updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ChatItemUpdated ->
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
||||
is CR.ChatItemReaction -> {
|
||||
if (active(r.user)) {
|
||||
chatModel.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
|
||||
withChats {
|
||||
updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ChatItemDeleted -> {
|
||||
is CR.ChatItemsDeleted -> {
|
||||
if (!active(r.user)) {
|
||||
if (r.toChatItem == null && r.deletedChatItem.chatItem.isRcvNew && r.deletedChatItem.chatInfo.ntfsEnabled) {
|
||||
chatModel.decreaseUnreadCounter(rhId, r.user)
|
||||
r.chatItemDeletions.forEach { (deletedChatItem, toChatItem) ->
|
||||
if (toChatItem == null && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled) {
|
||||
chatModel.decreaseUnreadCounter(rhId, r.user)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val cInfo = r.deletedChatItem.chatInfo
|
||||
val cItem = r.deletedChatItem.chatItem
|
||||
AudioPlayer.stop(cItem)
|
||||
val isLastChatItem = chatModel.getChat(cInfo.id)?.chatItems?.lastOrNull()?.id == cItem.id
|
||||
if (isLastChatItem && ntfManager.hasNotificationsForChat(cInfo.id)) {
|
||||
ntfManager.cancelNotificationsForChat(cInfo.id)
|
||||
ntfManager.displayNotification(
|
||||
r.user,
|
||||
cInfo.id,
|
||||
cInfo.displayName,
|
||||
generalGetString(if (r.toChatItem != null) MR.strings.marked_deleted_description else MR.strings.deleted_description)
|
||||
)
|
||||
}
|
||||
if (r.toChatItem == null) {
|
||||
chatModel.removeChatItem(rhId, cInfo, cItem)
|
||||
} else {
|
||||
chatModel.upsertChatItem(rhId, cInfo, r.toChatItem.chatItem)
|
||||
r.chatItemDeletions.forEach { (deletedChatItem, toChatItem) ->
|
||||
val cInfo = deletedChatItem.chatInfo
|
||||
val cItem = deletedChatItem.chatItem
|
||||
AudioPlayer.stop(cItem)
|
||||
val isLastChatItem = chatModel.getChat(cInfo.id)?.chatItems?.lastOrNull()?.id == cItem.id
|
||||
if (isLastChatItem && ntfManager.hasNotificationsForChat(cInfo.id)) {
|
||||
ntfManager.cancelNotificationsForChat(cInfo.id)
|
||||
ntfManager.displayNotification(
|
||||
r.user,
|
||||
cInfo.id,
|
||||
cInfo.displayName,
|
||||
generalGetString(if (toChatItem != null) MR.strings.marked_deleted_description else MR.strings.deleted_description)
|
||||
)
|
||||
}
|
||||
withChats {
|
||||
if (toChatItem == null) {
|
||||
removeChatItem(rhId, cInfo, cItem)
|
||||
} else {
|
||||
upsertChatItem(rhId, cInfo, toChatItem.chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.ReceivedGroupInvitation -> {
|
||||
if (active(r.user)) {
|
||||
chatModel.updateGroup(rhId, r.groupInfo) // update so that repeat group invitations are not duplicated
|
||||
withChats {
|
||||
// update so that repeat group invitations are not duplicated
|
||||
updateGroup(rhId, r.groupInfo)
|
||||
}
|
||||
// TODO NtfManager.shared.notifyGroupInvitation
|
||||
}
|
||||
}
|
||||
is CR.UserAcceptedGroupSent -> {
|
||||
if (!active(r.user)) return
|
||||
|
||||
chatModel.updateGroup(rhId, r.groupInfo)
|
||||
val conn = r.hostContact?.activeConn
|
||||
if (conn != null) {
|
||||
chatModel.replaceConnReqView(conn.id, "#${r.groupInfo.groupId}")
|
||||
chatModel.removeChat(rhId, conn.id)
|
||||
withChats {
|
||||
updateGroup(rhId, r.groupInfo)
|
||||
val conn = r.hostContact?.activeConn
|
||||
if (conn != null) {
|
||||
chatModel.replaceConnReqView(conn.id, "#${r.groupInfo.groupId}")
|
||||
removeChat(rhId, conn.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.GroupLinkConnecting -> {
|
||||
if (!active(r.user)) return
|
||||
|
||||
chatModel.updateGroup(rhId, r.groupInfo)
|
||||
val hostConn = r.hostMember.activeConn
|
||||
if (hostConn != null) {
|
||||
chatModel.replaceConnReqView(hostConn.id, "#${r.groupInfo.groupId}")
|
||||
chatModel.removeChat(rhId, hostConn.id)
|
||||
withChats {
|
||||
updateGroup(rhId, r.groupInfo)
|
||||
val hostConn = r.hostMember.activeConn
|
||||
if (hostConn != null) {
|
||||
chatModel.replaceConnReqView(hostConn.id, "#${r.groupInfo.groupId}")
|
||||
removeChat(rhId, hostConn.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.JoinedGroupMemberConnecting ->
|
||||
if (active(r.user)) {
|
||||
chatModel.upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.DeletedMemberUser -> // TODO update user member
|
||||
if (active(r.user)) {
|
||||
chatModel.updateGroup(rhId, r.groupInfo)
|
||||
withChats {
|
||||
updateGroup(rhId, r.groupInfo)
|
||||
}
|
||||
}
|
||||
is CR.DeletedMember ->
|
||||
if (active(r.user)) {
|
||||
chatModel.upsertGroupMember(rhId, r.groupInfo, r.deletedMember)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.deletedMember)
|
||||
}
|
||||
}
|
||||
is CR.LeftMember ->
|
||||
if (active(r.user)) {
|
||||
chatModel.upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.MemberRole ->
|
||||
if (active(r.user)) {
|
||||
chatModel.upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.MemberRoleUser ->
|
||||
if (active(r.user)) {
|
||||
chatModel.upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.MemberBlockedForAll ->
|
||||
if (active(r.user)) {
|
||||
chatModel.upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.GroupDeleted -> // TODO update user member
|
||||
if (active(r.user)) {
|
||||
chatModel.updateGroup(rhId, r.groupInfo)
|
||||
withChats {
|
||||
updateGroup(rhId, r.groupInfo)
|
||||
}
|
||||
}
|
||||
is CR.UserJoinedGroup ->
|
||||
if (active(r.user)) {
|
||||
chatModel.updateGroup(rhId, r.groupInfo)
|
||||
withChats {
|
||||
updateGroup(rhId, r.groupInfo)
|
||||
}
|
||||
}
|
||||
is CR.JoinedGroupMember ->
|
||||
if (active(r.user)) {
|
||||
chatModel.upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.ConnectedToGroupMember -> {
|
||||
if (active(r.user)) {
|
||||
chatModel.upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
if (r.memberContact != null) {
|
||||
chatModel.setContactNetworkStatus(r.memberContact, NetworkStatus.Connected())
|
||||
@@ -2207,11 +2301,15 @@ object ChatController {
|
||||
}
|
||||
is CR.GroupUpdated ->
|
||||
if (active(r.user)) {
|
||||
chatModel.updateGroup(rhId, r.toGroup)
|
||||
withChats {
|
||||
updateGroup(rhId, r.toGroup)
|
||||
}
|
||||
}
|
||||
is CR.NewMemberContactReceivedInv ->
|
||||
if (active(r.user)) {
|
||||
chatModel.updateContact(rhId, r.contact)
|
||||
withChats {
|
||||
updateContact(rhId, r.contact)
|
||||
}
|
||||
}
|
||||
is CR.RcvFileStart ->
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
||||
@@ -2311,19 +2409,27 @@ object ChatController {
|
||||
}
|
||||
is CR.ContactSwitch ->
|
||||
if (active(r.user)) {
|
||||
chatModel.updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats)
|
||||
withChats {
|
||||
updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats)
|
||||
}
|
||||
}
|
||||
is CR.GroupMemberSwitch ->
|
||||
if (active(r.user)) {
|
||||
chatModel.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.switchProgress.connectionStats)
|
||||
withChats {
|
||||
updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.switchProgress.connectionStats)
|
||||
}
|
||||
}
|
||||
is CR.ContactRatchetSync ->
|
||||
if (active(r.user)) {
|
||||
chatModel.updateContactConnectionStats(rhId, r.contact, r.ratchetSyncProgress.connectionStats)
|
||||
withChats {
|
||||
updateContactConnectionStats(rhId, r.contact, r.ratchetSyncProgress.connectionStats)
|
||||
}
|
||||
}
|
||||
is CR.GroupMemberRatchetSync ->
|
||||
if (active(r.user)) {
|
||||
chatModel.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats)
|
||||
withChats {
|
||||
updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats)
|
||||
}
|
||||
}
|
||||
is CR.RemoteHostSessionCode -> {
|
||||
chatModel.remoteHostPairing.value = r.remoteHost_ to RemoteHostSessionState.PendingConfirmation(r.sessionCode)
|
||||
@@ -2335,7 +2441,9 @@ object ChatController {
|
||||
}
|
||||
is CR.ContactDisabled -> {
|
||||
if (active(r.user)) {
|
||||
chatModel.updateContact(rhId, r.contact)
|
||||
withChats {
|
||||
updateContact(rhId, r.contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CR.RemoteHostStopped -> {
|
||||
@@ -2456,7 +2564,9 @@ object ChatController {
|
||||
}
|
||||
is CR.ContactPQEnabled ->
|
||||
if (active(r.user)) {
|
||||
chatModel.updateContact(rhId, r.contact)
|
||||
withChats {
|
||||
updateContact(rhId, r.contact)
|
||||
}
|
||||
}
|
||||
is CR.ChatRespError -> when {
|
||||
r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.CRITICAL -> {
|
||||
@@ -2532,7 +2642,9 @@ object ChatController {
|
||||
suspend fun leaveGroup(rh: Long?, groupId: Long) {
|
||||
val groupInfo = apiLeaveGroup(rh, groupId)
|
||||
if (groupInfo != null) {
|
||||
chatModel.updateGroup(rh, groupInfo)
|
||||
withChats {
|
||||
updateGroup(rh, groupInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2542,7 +2654,7 @@ object ChatController {
|
||||
val notify = { ntfManager.notifyMessageReceived(user, cInfo, cItem) }
|
||||
if (!activeUser(rh, user)) {
|
||||
notify()
|
||||
} else if (chatModel.upsertChatItem(rh, cInfo, cItem)) {
|
||||
} else if (withChats { upsertChatItem(rh, cInfo, cItem) }) {
|
||||
notify()
|
||||
} else if (cItem.content is CIContent.RcvCall && cItem.content.status == CICallStatus.Missed) {
|
||||
notify()
|
||||
@@ -2586,7 +2698,9 @@ object ChatController {
|
||||
chatModel.currentUser.value = user
|
||||
if (user == null) {
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chats.clear()
|
||||
withChats {
|
||||
chats.clear()
|
||||
}
|
||||
}
|
||||
val statuses = apiGetNetworkStatuses(rhId)
|
||||
if (statuses != null) {
|
||||
@@ -2737,8 +2851,8 @@ sealed class CC {
|
||||
class ApiSendMessage(val type: ChatType, val id: Long, val file: CryptoFile?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean, val ttl: Int?): CC()
|
||||
class ApiCreateChatItem(val noteFolderId: Long, val file: CryptoFile?, val mc: MsgContent): CC()
|
||||
class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent, val live: Boolean): CC()
|
||||
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC()
|
||||
class ApiDeleteMemberChatItem(val groupId: Long, val groupMemberId: Long, val itemId: Long): CC()
|
||||
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemIds: List<Long>, val mode: CIDeleteMode): CC()
|
||||
class ApiDeleteMemberChatItem(val groupId: Long, val itemIds: List<Long>): CC()
|
||||
class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC()
|
||||
class ApiForwardChatItem(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemId: Long, val ttl: Int?): CC()
|
||||
class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC()
|
||||
@@ -2787,7 +2901,7 @@ sealed class CC {
|
||||
class APIConnectPlan(val userId: Long, val connReq: String): CC()
|
||||
class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC()
|
||||
class ApiConnectContactViaAddress(val userId: Long, val incognito: Boolean, val contactId: Long): CC()
|
||||
class ApiDeleteChat(val type: ChatType, val id: Long, val notify: Boolean?): CC()
|
||||
class ApiDeleteChat(val type: ChatType, val id: Long, val chatDeleteMode: ChatDeleteMode): CC()
|
||||
class ApiClearChat(val type: ChatType, val id: Long): CC()
|
||||
class ApiListContacts(val userId: Long): CC()
|
||||
class ApiUpdateProfile(val userId: Long, val profile: Profile): CC()
|
||||
@@ -2887,8 +3001,8 @@ sealed class CC {
|
||||
"/_create *$noteFolderId json ${json.encodeToString(ComposedMessage(file, null, mc))}"
|
||||
}
|
||||
is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${mc.cmdString}"
|
||||
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}"
|
||||
is ApiDeleteMemberChatItem -> "/_delete member item #$groupId $groupMemberId $itemId"
|
||||
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} ${itemIds.joinToString(",")} ${mode.deleteMode}"
|
||||
is ApiDeleteMemberChatItem -> "/_delete member item #$groupId ${itemIds.joinToString(",")}"
|
||||
is ApiChatItemReaction -> "/_reaction ${chatRef(type, id)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}"
|
||||
is ApiForwardChatItem -> {
|
||||
val ttlStr = if (ttl != null) "$ttl" else "default"
|
||||
@@ -2940,11 +3054,7 @@ sealed class CC {
|
||||
is APIConnectPlan -> "/_connect plan $userId $connReq"
|
||||
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq"
|
||||
is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId"
|
||||
is ApiDeleteChat -> if (notify != null) {
|
||||
"/_delete ${chatRef(type, id)} notify=${onOff(notify)}"
|
||||
} else {
|
||||
"/_delete ${chatRef(type, id)}"
|
||||
}
|
||||
is ApiDeleteChat -> "/_delete ${chatRef(type, id)} ${chatDeleteMode.cmdString}"
|
||||
is ApiClearChat -> "/_clear chat ${chatRef(type, id)}"
|
||||
is ApiListContacts -> "/_contacts $userId"
|
||||
is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}"
|
||||
@@ -3166,8 +3276,6 @@ sealed class CC {
|
||||
null
|
||||
}
|
||||
|
||||
private fun onOff(b: Boolean): String = if (b) "on" else "off"
|
||||
|
||||
private fun maybePwd(pwd: String?): String = if (pwd == "" || pwd == null) "" else " " + json.encodeToString(pwd)
|
||||
|
||||
companion object {
|
||||
@@ -3177,6 +3285,8 @@ sealed class CC {
|
||||
}
|
||||
}
|
||||
|
||||
fun onOff(b: Boolean): String = if (b) "on" else "off"
|
||||
|
||||
@Serializable
|
||||
data class NewUser(
|
||||
val profile: Profile?,
|
||||
@@ -4679,7 +4789,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val user: UserRef, val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("chatItemNotChanged") class ChatItemNotChanged(val user: UserRef, val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("chatItemReaction") class ChatItemReaction(val user: UserRef, val added: Boolean, val reaction: ACIReaction): CR()
|
||||
@Serializable @SerialName("chatItemDeleted") class ChatItemDeleted(val user: UserRef, val deletedChatItem: AChatItem, val toChatItem: AChatItem? = null, val byUser: Boolean): CR()
|
||||
@Serializable @SerialName("chatItemsDeleted") class ChatItemsDeleted(val user: UserRef, val chatItemDeletions: List<ChatItemDeletion>, val byUser: Boolean): CR()
|
||||
// group events
|
||||
@Serializable @SerialName("groupCreated") class GroupCreated(val user: UserRef, val groupInfo: GroupInfo): CR()
|
||||
@Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val user: UserRef, val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR()
|
||||
@@ -4772,6 +4882,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("cmdOk") class CmdOk(val user: UserRef?): CR()
|
||||
@Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: UserRef?, val chatError: ChatError): CR()
|
||||
@Serializable @SerialName("chatError") class ChatRespError(val user_: UserRef?, val chatError: ChatError): CR()
|
||||
@Serializable @SerialName("archiveExported") class ArchiveExported(val archiveErrors: List<ArchiveError>): CR()
|
||||
@Serializable @SerialName("archiveImported") class ArchiveImported(val archiveErrors: List<ArchiveError>): CR()
|
||||
@Serializable @SerialName("appSettings") class AppSettingsR(val appSettings: AppSettings): CR()
|
||||
@Serializable @SerialName("agentSubsTotal") class AgentSubsTotal(val user: UserRef, val subsTotal: SMPServerSubs, val hasSession: Boolean): CR()
|
||||
@@ -4854,7 +4965,7 @@ sealed class CR {
|
||||
is ChatItemUpdated -> "chatItemUpdated"
|
||||
is ChatItemNotChanged -> "chatItemNotChanged"
|
||||
is ChatItemReaction -> "chatItemReaction"
|
||||
is ChatItemDeleted -> "chatItemDeleted"
|
||||
is ChatItemsDeleted -> "chatItemsDeleted"
|
||||
is GroupCreated -> "groupCreated"
|
||||
is SentGroupInvitation -> "sentGroupInvitation"
|
||||
is UserAcceptedGroupSent -> "userAcceptedGroupSent"
|
||||
@@ -4941,6 +5052,7 @@ sealed class CR {
|
||||
is CmdOk -> "cmdOk"
|
||||
is ChatCmdError -> "chatCmdError"
|
||||
is ChatRespError -> "chatError"
|
||||
is ArchiveExported -> "archiveExported"
|
||||
is ArchiveImported -> "archiveImported"
|
||||
is AppSettingsR -> "appSettings"
|
||||
is Response -> "* $type"
|
||||
@@ -5021,7 +5133,7 @@ sealed class CR {
|
||||
is ChatItemUpdated -> withUser(user, json.encodeToString(chatItem))
|
||||
is ChatItemNotChanged -> withUser(user, json.encodeToString(chatItem))
|
||||
is ChatItemReaction -> withUser(user, "added: $added\n${json.encodeToString(reaction)}")
|
||||
is ChatItemDeleted -> withUser(user, "deletedChatItem:\n${json.encodeToString(deletedChatItem)}\ntoChatItem:\n${json.encodeToString(toChatItem)}\nbyUser: $byUser")
|
||||
is ChatItemsDeleted -> withUser(user, "${chatItemDeletions.map { (deletedChatItem, toChatItem) -> "deletedChatItem: ${json.encodeToString(deletedChatItem)}\ntoChatItem: ${json.encodeToString(toChatItem)}" }} \nbyUser: $byUser")
|
||||
is GroupCreated -> withUser(user, json.encodeToString(groupInfo))
|
||||
is SentGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmember: $member")
|
||||
is UserAcceptedGroupSent -> json.encodeToString(groupInfo)
|
||||
@@ -5125,6 +5237,7 @@ sealed class CR {
|
||||
is CmdOk -> withUser(user, noDetails())
|
||||
is ChatCmdError -> withUser(user_, chatError.string)
|
||||
is ChatRespError -> withUser(user_, chatError.string)
|
||||
is ArchiveExported -> "${archiveErrors.map { it.string } }"
|
||||
is ArchiveImported -> "${archiveErrors.map { it.string } }"
|
||||
is AppSettingsR -> json.encodeToString(appSettings)
|
||||
is Response -> json
|
||||
@@ -5144,6 +5257,19 @@ fun chatError(r: CR): ChatErrorType? {
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class ChatDeleteMode {
|
||||
@Serializable @SerialName("full") class Full(val notify: Boolean): ChatDeleteMode()
|
||||
@Serializable @SerialName("entity") class Entity(val notify: Boolean): ChatDeleteMode()
|
||||
@Serializable @SerialName("messages") class Messages: ChatDeleteMode()
|
||||
|
||||
val cmdString: String get() = when (this) {
|
||||
is ChatDeleteMode.Full -> "full notify=${onOff(notify)}"
|
||||
is ChatDeleteMode.Entity -> "entity notify=${onOff(notify)}"
|
||||
is ChatDeleteMode.Messages -> "messages"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class ConnectionPlan {
|
||||
@Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan()
|
||||
@@ -5917,11 +6043,11 @@ sealed class RCErrorType {
|
||||
@Serializable
|
||||
sealed class ArchiveError {
|
||||
val string: String get() = when (this) {
|
||||
is ArchiveErrorImport -> "import ${chatError.string}"
|
||||
is ArchiveErrorImportFile -> "importFile $file ${chatError.string}"
|
||||
is ArchiveErrorImport -> "import ${importError}"
|
||||
is ArchiveErrorFile -> "importFile $file ${fileError}"
|
||||
}
|
||||
@Serializable @SerialName("import") class ArchiveErrorImport(val chatError: ChatError): ArchiveError()
|
||||
@Serializable @SerialName("importFile") class ArchiveErrorImportFile(val file: String, val chatError: ChatError): ArchiveError()
|
||||
@Serializable @SerialName("import") class ArchiveErrorImport(val importError: String): ArchiveError()
|
||||
@Serializable @SerialName("fileError") class ArchiveErrorFile(val file: String, val fileError: String): ArchiveError()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -6031,6 +6157,7 @@ data class AppSettings(
|
||||
var uiDarkColorScheme: String? = null,
|
||||
var uiCurrentThemeIds: Map<String, String>? = null,
|
||||
var uiThemes: List<ThemeOverrides>? = null,
|
||||
var oneHandUI: Boolean? = null
|
||||
) {
|
||||
fun prepareForExport(): AppSettings {
|
||||
val empty = AppSettings()
|
||||
@@ -6061,6 +6188,7 @@ data class AppSettings(
|
||||
if (uiDarkColorScheme != def.uiDarkColorScheme) { empty.uiDarkColorScheme = uiDarkColorScheme }
|
||||
if (uiCurrentThemeIds != def.uiCurrentThemeIds) { empty.uiCurrentThemeIds = uiCurrentThemeIds }
|
||||
if (uiThemes != def.uiThemes) { empty.uiThemes = uiThemes }
|
||||
if (oneHandUI != def.oneHandUI) { empty.oneHandUI = oneHandUI }
|
||||
return empty
|
||||
}
|
||||
|
||||
@@ -6099,6 +6227,7 @@ data class AppSettings(
|
||||
uiDarkColorScheme?.let { def.systemDarkTheme.set(it) }
|
||||
uiCurrentThemeIds?.let { def.currentThemeIds.set(it) }
|
||||
uiThemes?.let { def.themeOverrides.set(it.skipDuplicates()) }
|
||||
oneHandUI?.let { def.oneHandUI.set(it) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -6130,6 +6259,7 @@ data class AppSettings(
|
||||
uiDarkColorScheme = DefaultTheme.SIMPLEX.themeName,
|
||||
uiCurrentThemeIds = null,
|
||||
uiThemes = null,
|
||||
oneHandUI = false
|
||||
)
|
||||
|
||||
val current: AppSettings
|
||||
@@ -6162,6 +6292,7 @@ data class AppSettings(
|
||||
uiDarkColorScheme = def.systemDarkTheme.get() ?: DefaultTheme.SIMPLEX.themeName,
|
||||
uiCurrentThemeIds = def.currentThemeIds.get(),
|
||||
uiThemes = def.themeOverrides.get(),
|
||||
oneHandUI = def.oneHandUI.get()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -6,6 +6,7 @@ import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import chat.simplex.common.model.ChatId
|
||||
import chat.simplex.common.model.NotificationsMode
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -20,6 +21,7 @@ interface PlatformInterface {
|
||||
fun androidChatInitializedAndStarted() {}
|
||||
fun androidIsBackgroundCallAllowed(): Boolean = true
|
||||
fun androidSetNightModeIfSupported() {}
|
||||
fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) {}
|
||||
fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {}
|
||||
fun androidPictureInPictureAllowed(): Boolean = true
|
||||
fun androidCallEnded() {}
|
||||
|
||||
+7
@@ -1,6 +1,7 @@
|
||||
package chat.simplex.common.ui.theme
|
||||
|
||||
import androidx.compose.material.Colors
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
@@ -103,6 +104,8 @@ object ThemeManager {
|
||||
appPrefs.currentTheme.set(theme)
|
||||
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||
platform.androidSetNightModeIfSupported()
|
||||
val c = CurrentColors.value.colors
|
||||
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !ChatController.appPrefs.oneHandUI.get(), ChatController.appPrefs.oneHandUI.get())
|
||||
}
|
||||
|
||||
fun changeDarkTheme(theme: String) {
|
||||
@@ -120,6 +123,10 @@ object ThemeManager {
|
||||
themeIds[nonSystemThemeName] = prevValue.themeId
|
||||
appPrefs.currentThemeIds.set(themeIds)
|
||||
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||
if (name == ThemeColor.BACKGROUND) {
|
||||
val c = CurrentColors.value.colors
|
||||
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, false, false)
|
||||
}
|
||||
}
|
||||
|
||||
fun applyThemeColor(name: ThemeColor, color: Color? = null, pref: MutableState<ThemeModeOverride>) {
|
||||
|
||||
+463
-46
@@ -12,18 +12,23 @@ import SectionView
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.*
|
||||
import chat.simplex.common.views.call.CallMediaType
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import androidx.compose.ui.text.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -31,6 +36,7 @@ import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
@@ -54,10 +60,11 @@ fun ChatInfoView(
|
||||
localAlias: String,
|
||||
connectionCode: String?,
|
||||
close: () -> Unit,
|
||||
onSearchClicked: () -> Unit
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
val contact = rememberUpdatedState(contact).value
|
||||
val chat = remember(contact.id) { chatModel.chats.firstOrNull { it.id == contact.id } }
|
||||
val chat = remember(contact.id) { chatModel.chats.value.firstOrNull { it.id == contact.id } }
|
||||
val currentUser = remember { chatModel.currentUser }.value
|
||||
val connStats = remember(contact.id, connectionStats) { mutableStateOf(connectionStats) }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
@@ -74,7 +81,7 @@ fun ChatInfoView(
|
||||
sendReceipts = sendReceipts,
|
||||
setSendReceipts = { sendRcpts ->
|
||||
val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool)
|
||||
updateChatSettings(chat, chatSettings, chatModel)
|
||||
updateChatSettings(chat.remoteHostId, chat.chatInfo, chatSettings, chatModel)
|
||||
sendReceipts.value = sendRcpts
|
||||
},
|
||||
connStats = connStats,
|
||||
@@ -102,7 +109,9 @@ fun ChatInfoView(
|
||||
val cStats = chatModel.controller.apiSwitchContact(chatRh, contact.contactId)
|
||||
connStats.value = cStats
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
||||
withChats {
|
||||
updateContactConnectionStats(chatRh, contact, cStats)
|
||||
}
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
@@ -114,7 +123,9 @@ fun ChatInfoView(
|
||||
val cStats = chatModel.controller.apiAbortSwitchContact(chatRh, contact.contactId)
|
||||
connStats.value = cStats
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
||||
withChats {
|
||||
updateContactConnectionStats(chatRh, contact, cStats)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -124,7 +135,9 @@ fun ChatInfoView(
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
|
||||
connStats.value = cStats
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
||||
withChats {
|
||||
updateContactConnectionStats(chatRh, contact, cStats)
|
||||
}
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
@@ -135,7 +148,9 @@ fun ChatInfoView(
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = true)
|
||||
connStats.value = cStats
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
||||
withChats {
|
||||
updateContactConnectionStats(chatRh, contact, cStats)
|
||||
}
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
@@ -151,14 +166,16 @@ fun ChatInfoView(
|
||||
verify = { code ->
|
||||
chatModel.controller.apiVerifyContact(chatRh, ct.contactId, code)?.let { r ->
|
||||
val (verified, existingCode) = r
|
||||
chatModel.updateContact(
|
||||
chatRh,
|
||||
ct.copy(
|
||||
activeConn = ct.activeConn?.copy(
|
||||
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
|
||||
withChats {
|
||||
updateContact(
|
||||
chatRh,
|
||||
ct.copy(
|
||||
activeConn = ct.activeConn?.copy(
|
||||
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
r
|
||||
}
|
||||
},
|
||||
@@ -166,7 +183,9 @@ fun ChatInfoView(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
close = close,
|
||||
onSearchClicked = onSearchClicked
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -201,34 +220,42 @@ sealed class SendReceipts {
|
||||
|
||||
fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
val chatInfo = chat.chatInfo
|
||||
if (chatInfo is ChatInfo.Direct) {
|
||||
val contact = chatInfo.contact
|
||||
when {
|
||||
contact.sndReady && contact.active && !chatInfo.chatDeleted ->
|
||||
deleteContactOrConversationDialog(chat, contact, chatModel, close)
|
||||
|
||||
contact.sndReady && contact.active && chatInfo.chatDeleted ->
|
||||
deleteContactWithoutConversation(chat, chatModel, close)
|
||||
|
||||
else -> // !(contact.sndReady && contact.active)
|
||||
deleteNotReadyContact(chat, chatModel, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteContactOrConversationDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)?) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.delete_contact_question),
|
||||
text = AnnotatedString(generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning)),
|
||||
buttons = {
|
||||
Column {
|
||||
if (chatInfo is ChatInfo.Direct && chatInfo.contact.sndReady && chatInfo.contact.active) {
|
||||
// Delete and notify contact
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, notify = true)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
// Delete
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, notify = false)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
} else {
|
||||
// Delete
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
// Only delete conversation
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, chatDeleteMode = ChatDeleteMode.Messages())
|
||||
if (chatModel.controller.appPrefs.showDeleteConversationNotice.get()) {
|
||||
showDeleteConversationNotice(contact)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.only_delete_conversation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
// Delete contact
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteActiveContactDialog(chat, contact, chatModel, close)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.button_delete_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
// Cancel
|
||||
SectionItemView({
|
||||
@@ -241,13 +268,207 @@ fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? =
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, notify: Boolean? = null) {
|
||||
private fun showDeleteConversationNotice(contact: Contact) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.conversation_deleted),
|
||||
text = String.format(generalGetString(MR.strings.you_can_still_send_messages_to_contact), contact.displayName),
|
||||
confirmText = generalGetString(MR.strings.ok),
|
||||
dismissText = generalGetString(MR.strings.dont_show_again),
|
||||
onDismiss = {
|
||||
chatModel.controller.appPrefs.showDeleteConversationNotice.set(false)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
sealed class ContactDeleteMode {
|
||||
class Full: ContactDeleteMode()
|
||||
class Entity: ContactDeleteMode()
|
||||
|
||||
fun toChatDeleteMode(notify: Boolean): ChatDeleteMode =
|
||||
when (this) {
|
||||
is Full -> ChatDeleteMode.Full(notify)
|
||||
is Entity -> ChatDeleteMode.Entity(notify)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteActiveContactDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
val contactDeleteMode = mutableStateOf<ContactDeleteMode>(ContactDeleteMode.Full())
|
||||
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.delete_contact_question),
|
||||
text = generalGetString(MR.strings.delete_contact_cannot_undo_warning),
|
||||
buttons = {
|
||||
Column {
|
||||
// Keep conversation toggle
|
||||
SectionItemView {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(stringResource(MR.strings.keep_conversation))
|
||||
Spacer(Modifier.width(DEFAULT_PADDING))
|
||||
DefaultSwitch(
|
||||
checked = contactDeleteMode.value is ContactDeleteMode.Entity,
|
||||
onCheckedChange = {
|
||||
contactDeleteMode.value =
|
||||
if (it) ContactDeleteMode.Entity() else ContactDeleteMode.Full()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
// Delete without notification
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.value.toChatDeleteMode(notify = false))
|
||||
if (contactDeleteMode.value is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) {
|
||||
showDeleteContactNotice(contact)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_without_notification), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
// Delete contact and notify
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.value.toChatDeleteMode(notify = true))
|
||||
if (contactDeleteMode.value is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) {
|
||||
showDeleteContactNotice(contact)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
// Cancel
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun deleteContactWithoutConversation(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.confirm_delete_contact_question),
|
||||
text = generalGetString(MR.strings.delete_contact_cannot_undo_warning),
|
||||
buttons = {
|
||||
Column {
|
||||
// Delete and notify contact
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(
|
||||
chat,
|
||||
chatModel,
|
||||
close,
|
||||
chatDeleteMode = ContactDeleteMode.Full().toChatDeleteMode(notify = true)
|
||||
)
|
||||
}) {
|
||||
Text(
|
||||
generalGetString(MR.strings.delete_and_notify_contact),
|
||||
Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
// Delete without notification
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(
|
||||
chat,
|
||||
chatModel,
|
||||
close,
|
||||
chatDeleteMode = ContactDeleteMode.Full().toChatDeleteMode(notify = false)
|
||||
)
|
||||
}) {
|
||||
Text(
|
||||
generalGetString(MR.strings.delete_without_notification),
|
||||
Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
}
|
||||
// Cancel
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(
|
||||
stringResource(MR.strings.cancel_verb),
|
||||
Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun deleteNotReadyContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.confirm_delete_contact_question),
|
||||
text = generalGetString(MR.strings.delete_contact_cannot_undo_warning),
|
||||
buttons = {
|
||||
// Confirm
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(
|
||||
chat,
|
||||
chatModel,
|
||||
close,
|
||||
chatDeleteMode = ContactDeleteMode.Full().toChatDeleteMode(notify = false)
|
||||
)
|
||||
}) {
|
||||
Text(
|
||||
generalGetString(MR.strings.confirm_verb),
|
||||
Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
// Cancel
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(
|
||||
stringResource(MR.strings.cancel_verb),
|
||||
Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun showDeleteContactNotice(contact: Contact) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.contact_deleted),
|
||||
text = String.format(generalGetString(MR.strings.you_can_still_view_conversation_with_contact), contact.displayName),
|
||||
confirmText = generalGetString(MR.strings.ok),
|
||||
dismissText = generalGetString(MR.strings.dont_show_again),
|
||||
onDismiss = {
|
||||
chatModel.controller.appPrefs.showDeleteContactNotice.set(false)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) {
|
||||
val chatInfo = chat.chatInfo
|
||||
withBGApi {
|
||||
val chatRh = chat.remoteHostId
|
||||
val r = chatModel.controller.apiDeleteChat(chatRh, chatInfo.chatType, chatInfo.apiId, notify)
|
||||
if (r) {
|
||||
chatModel.removeChat(chatRh, chatInfo.id)
|
||||
val ct = chatModel.controller.apiDeleteContact(chatRh, chatInfo.apiId, chatDeleteMode)
|
||||
if (ct != null) {
|
||||
withChats {
|
||||
when (chatDeleteMode) {
|
||||
is ChatDeleteMode.Full ->
|
||||
removeChat(chatRh, chatInfo.id)
|
||||
is ChatDeleteMode.Entity ->
|
||||
updateContact(chatRh, ct)
|
||||
is ChatDeleteMode.Messages ->
|
||||
clearChat(chatRh, ChatInfo.Direct(ct))
|
||||
}
|
||||
}
|
||||
if (chatModel.chatId.value == chatInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
@@ -300,6 +521,8 @@ fun ChatInfoLayout(
|
||||
syncContactConnection: () -> Unit,
|
||||
syncContactConnectionForce: () -> Unit,
|
||||
verifyClicked: () -> Unit,
|
||||
close: () -> Unit,
|
||||
onSearchClicked: () -> Unit
|
||||
) {
|
||||
val cStats = connStats.value
|
||||
val scrollState = rememberScrollState()
|
||||
@@ -319,7 +542,27 @@ fun ChatInfoLayout(
|
||||
}
|
||||
|
||||
LocalAliasEditor(chat.id, localAlias, updateValue = onLocalAliasChanged)
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
SearchButton(chat, contact, close, onSearchClicked)
|
||||
Spacer(Modifier.weight(1f))
|
||||
AudioCallButton(chat, contact)
|
||||
Spacer(Modifier.weight(1f))
|
||||
VideoButton(chat, contact)
|
||||
Spacer(Modifier.weight(1f))
|
||||
MuteButton(chat, contact)
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
if (customUserProfile != null) {
|
||||
SectionView(generalGetString(MR.strings.incognito).uppercase()) {
|
||||
SectionItemViewSpaceBetween {
|
||||
@@ -347,7 +590,7 @@ fun ChatInfoLayout(
|
||||
|
||||
WallpaperButton {
|
||||
ModalManager.end.showModal {
|
||||
val chat = remember { derivedStateOf { chatModel.chats.firstOrNull { it.id == chat.id } } }
|
||||
val chat = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chat.id } } }
|
||||
val c = chat.value
|
||||
if (c != null) {
|
||||
ChatWallpaperEditorModal(c)
|
||||
@@ -535,6 +778,174 @@ fun LocalAliasEditor(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchButton(chat: Chat, contact: Contact, close: () -> Unit, onSearchClicked: () -> Unit) {
|
||||
val disabled = !contact.ready || chat.chatItems.isEmpty()
|
||||
InfoViewActionButton(
|
||||
icon = painterResource(MR.images.ic_search),
|
||||
title = generalGetString(MR.strings.info_view_search_button),
|
||||
disabled = disabled,
|
||||
disabledLook = disabled,
|
||||
onClick = {
|
||||
if (appPlatform.isAndroid) {
|
||||
close.invoke()
|
||||
}
|
||||
onSearchClicked()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MuteButton(chat: Chat, contact: Contact) {
|
||||
val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) }
|
||||
val disabled = !contact.ready || !contact.active
|
||||
|
||||
InfoViewActionButton(
|
||||
icon = if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications),
|
||||
title = if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat),
|
||||
disabled = disabled,
|
||||
disabledLook = disabled,
|
||||
onClick = {
|
||||
toggleNotifications(chat.remoteHostId, chat.chatInfo, !ntfsEnabled.value, chatModel, ntfsEnabled)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AudioCallButton(chat: Chat, contact: Contact) {
|
||||
CallButton(
|
||||
chat,
|
||||
contact,
|
||||
icon = painterResource(MR.images.ic_call),
|
||||
title = generalGetString(MR.strings.info_view_call_button),
|
||||
mediaType = CallMediaType.Audio
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VideoButton(chat: Chat, contact: Contact) {
|
||||
CallButton(
|
||||
chat,
|
||||
contact,
|
||||
icon = painterResource(MR.images.ic_videocam),
|
||||
title = generalGetString(MR.strings.info_view_video_button),
|
||||
mediaType = CallMediaType.Video
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CallButton(chat: Chat, contact: Contact, icon: Painter, title: String, mediaType: CallMediaType) {
|
||||
val canCall = contact.ready && contact.active && contact.mergedPreferences.calls.enabled.forUser && chatModel.activeCall.value == null
|
||||
val needToAllowCallsToContact = remember(chat.chatInfo) {
|
||||
chat.chatInfo is ChatInfo.Direct && with(chat.chatInfo.contact.mergedPreferences.calls) {
|
||||
((userPreference as? ContactUserPref.User)?.preference?.allow == FeatureAllowed.NO || (userPreference as? ContactUserPref.Contact)?.preference?.allow == FeatureAllowed.NO) &&
|
||||
contactPreference.allow == FeatureAllowed.YES
|
||||
}
|
||||
}
|
||||
val allowedCallsByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Calls) }
|
||||
|
||||
InfoViewActionButton(
|
||||
icon = icon,
|
||||
title = title,
|
||||
disabled = chatModel.activeCall.value != null,
|
||||
disabledLook = !canCall,
|
||||
onClick =
|
||||
when {
|
||||
canCall -> { { startChatCall(chat.remoteHostId, chat.chatInfo, mediaType) } }
|
||||
contact.nextSendGrpInv -> { { showCantCallContactSendMessageAlert() } }
|
||||
!contact.active -> { { showCantCallContactDeletedAlert() } }
|
||||
!contact.ready -> { { showCantCallContactConnectingAlert() } }
|
||||
needToAllowCallsToContact -> { { showNeedToAllowCallsAlert(onConfirm = { allowCallsToContact(chat) }) } }
|
||||
!allowedCallsByPrefs -> { { showCallsProhibitedAlert() }}
|
||||
else -> { { AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.cant_call_contact_alert_title)) } }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun showCantCallContactSendMessageAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.cant_call_contact_alert_title),
|
||||
text = generalGetString(MR.strings.cant_call_member_send_message_alert_text)
|
||||
)
|
||||
}
|
||||
|
||||
private fun showCantCallContactConnectingAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.cant_call_contact_alert_title),
|
||||
text = generalGetString(MR.strings.cant_call_contact_connecting_wait_alert_text)
|
||||
)
|
||||
}
|
||||
|
||||
private fun showCantCallContactDeletedAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.cant_call_contact_alert_title),
|
||||
text = generalGetString(MR.strings.cant_call_contact_deleted_alert_text)
|
||||
)
|
||||
}
|
||||
|
||||
private fun showNeedToAllowCallsAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.allow_calls_question),
|
||||
text = generalGetString(MR.strings.you_need_to_allow_calls),
|
||||
confirmText = generalGetString(MR.strings.allow_verb),
|
||||
dismissText = generalGetString(MR.strings.cancel_verb),
|
||||
onConfirm = onConfirm,
|
||||
)
|
||||
}
|
||||
|
||||
private fun allowCallsToContact(chat: Chat) {
|
||||
val contact = (chat.chatInfo as ChatInfo.Direct?)?.contact ?: return
|
||||
withBGApi {
|
||||
chatModel.controller.allowFeatureToContact(chat.remoteHostId, contact, ChatFeature.Calls)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCallsProhibitedAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.calls_prohibited_alert_title),
|
||||
text = generalGetString(MR.strings.calls_prohibited_ask_to_enable_calls_alert_text)
|
||||
)
|
||||
}
|
||||
|
||||
// for ChatInfoView (it has most buttons - 4) we use Spacer(Modifier.weight(1f)) to fit,
|
||||
// for GroupChat And GroupMemberInfoViews (2 to 3 buttons) we use this as approximately equal to spacing in ChatInfoView
|
||||
val INFO_VIEW_BUTTONS_PADDING = 36.dp
|
||||
|
||||
@Composable
|
||||
fun InfoViewActionButton(icon: Painter, title: String, disabled: Boolean, disabledLook: Boolean, onClick: () -> Unit) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onClick,
|
||||
enabled = !disabled
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
if (disabledLook) MaterialTheme.colors.secondaryVariant else MaterialTheme.colors.primary,
|
||||
shape = CircleShape
|
||||
)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
Modifier.size(24.dp * fontSizeSqrtMultiplier),
|
||||
tint = if (disabledLook) MaterialTheme.colors.secondary else MaterialTheme.colors.onPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
title.capitalize(Locale.current),
|
||||
style = MaterialTheme.typography.subtitle2.copy(fontWeight = FontWeight.Normal),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier.padding(top = DEFAULT_SPACE_AFTER_ICON)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NetworkStatusRow(networkStatus: NetworkStatus) {
|
||||
Row(
|
||||
@@ -768,10 +1179,12 @@ suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, c
|
||||
wallpaperFilesToDelete.forEach(::removeWallpaperFile)
|
||||
|
||||
if (controller.apiSetChatUIThemes(chat.remoteHostId, chat.id, changedThemes)) {
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
chatModel.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes)))
|
||||
} else if (chat.chatInfo is ChatInfo.Group) {
|
||||
chatModel.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(groupInfo = chat.chatInfo.groupInfo.copy(uiThemes = changedThemes)))
|
||||
withChats {
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes)))
|
||||
} else if (chat.chatInfo is ChatInfo.Group) {
|
||||
updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(groupInfo = chat.chatInfo.groupInfo.copy(uiThemes = changedThemes)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -779,7 +1192,9 @@ suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, c
|
||||
private fun setContactAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withBGApi {
|
||||
val chatRh = chat.remoteHostId
|
||||
chatModel.controller.apiSetContactAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let {
|
||||
chatModel.updateContact(chatRh, it)
|
||||
withChats {
|
||||
updateContact(chatRh, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -847,6 +1262,8 @@ fun PreviewChatInfoLayout() {
|
||||
syncContactConnection = {},
|
||||
syncContactConnectionForce = {},
|
||||
verifyClicked = {},
|
||||
close = {},
|
||||
onSearchClicked = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+273
-240
@@ -11,7 +11,6 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.mapSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.platform.*
|
||||
@@ -25,6 +24,7 @@ import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.call.*
|
||||
import chat.simplex.common.views.chat.group.*
|
||||
@@ -44,85 +44,76 @@ import java.net.URI
|
||||
import kotlin.math.sign
|
||||
|
||||
@Composable
|
||||
fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: String) -> Unit) {
|
||||
val activeChat = remember { mutableStateOf(chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatId }) }
|
||||
val searchText = rememberSaveable { mutableStateOf("") }
|
||||
// staleChatId means the id that was before chatModel.chatId becomes null. It's needed for Android only to make transition from chat
|
||||
// to chat list smooth. Otherwise, chat view will become blank right before the transition starts
|
||||
fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -> Unit) {
|
||||
val shouldReturn = remember { mutableStateOf(false) }
|
||||
val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } }
|
||||
val showSearch = rememberSaveable { mutableStateOf(false) }
|
||||
val activeChatInfo = remember { derivedStateOf {
|
||||
val info = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatInfo
|
||||
if (info == null) {
|
||||
shouldReturn.value = true
|
||||
}
|
||||
return@derivedStateOf info ?: ChatInfo.Direct.sampleData
|
||||
}
|
||||
}
|
||||
val user = chatModel.currentUser.value
|
||||
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
|
||||
val composeState = rememberSaveable(saver = ComposeState.saver()) {
|
||||
val draft = chatModel.draft.value
|
||||
val sharedContent = chatModel.sharedContent.value
|
||||
mutableStateOf(
|
||||
if (chatModel.draftChatId.value == chatId && draft != null && (sharedContent !is SharedContent.Forward || sharedContent.fromChatInfo.id == chatId)) {
|
||||
draft
|
||||
} else {
|
||||
ComposeState(useLinkPreviews = useLinkPreviews)
|
||||
}
|
||||
)
|
||||
}
|
||||
val attachmentOption = rememberSaveable { mutableStateOf<AttachmentOption?>(null) }
|
||||
val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val scope = rememberCoroutineScope()
|
||||
LaunchedEffect(Unit) {
|
||||
// snapshotFlow here is because it reacts much faster on changes in chatModel.chatId.value.
|
||||
// With LaunchedEffect(chatModel.chatId.value) there is a noticeable delay before reconstruction of the view
|
||||
launch {
|
||||
snapshotFlow { chatModel.chatId.value }
|
||||
.distinctUntilChanged()
|
||||
.filterNotNull()
|
||||
.collect { chatId ->
|
||||
if (activeChat.value?.id != chatId) {
|
||||
// Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly
|
||||
// Also for situation when chatId changes after clicking in notification, etc
|
||||
activeChat.value = chatModel.getChat(chatId)
|
||||
}
|
||||
markUnreadChatAsRead(activeChat, chatModel)
|
||||
}
|
||||
if (shouldReturn.value || user == null) {
|
||||
LaunchedEffect(Unit) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
launch {
|
||||
snapshotFlow {
|
||||
/**
|
||||
* It's possible that in some cases concurrent modification can happen on [ChatModel.chats] list.
|
||||
* In this case only error log will be printed here (no crash).
|
||||
* TODO: Re-write [ChatModel.chats] logic to a new list assignment instead of changing content of mutableList to prevent that
|
||||
* */
|
||||
try {
|
||||
chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }
|
||||
} catch (e: ConcurrentModificationException) {
|
||||
Log.e(TAG, e.stackTraceToString())
|
||||
null
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
// Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions
|
||||
.filter { it != null && it.chatInfo != activeChat.value?.chatInfo }
|
||||
.collect {
|
||||
activeChat.value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
val view = LocalMultiplatformView()
|
||||
val chat = activeChat.value
|
||||
if (chat == null || user == null) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
} else {
|
||||
val chatRh = chat.remoteHostId
|
||||
val chatInfo = activeChatInfo.value
|
||||
val searchText = rememberSaveable { mutableStateOf("") }
|
||||
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
|
||||
val composeState = rememberSaveable(saver = ComposeState.saver()) {
|
||||
val draft = chatModel.draft.value
|
||||
val sharedContent = chatModel.sharedContent.value
|
||||
mutableStateOf(
|
||||
if (chatModel.draftChatId.value == staleChatId.value && draft != null && (sharedContent !is SharedContent.Forward || sharedContent.fromChatInfo.id == staleChatId.value)) {
|
||||
draft
|
||||
} else {
|
||||
ComposeState(useLinkPreviews = useLinkPreviews)
|
||||
}
|
||||
)
|
||||
}
|
||||
val attachmentOption = rememberSaveable { mutableStateOf<AttachmentOption?>(null) }
|
||||
val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val scope = rememberCoroutineScope()
|
||||
LaunchedEffect(Unit) {
|
||||
// snapshotFlow here is because it reacts much faster on changes in chatModel.chatId.value.
|
||||
// With LaunchedEffect(chatModel.chatId.value) there is a noticeable delay before reconstruction of the view
|
||||
launch {
|
||||
snapshotFlow { chatModel.chatId.value }
|
||||
.distinctUntilChanged()
|
||||
.filterNotNull()
|
||||
.collect { chatId ->
|
||||
markUnreadChatAsRead(chatId)
|
||||
showSearch.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
val view = LocalMultiplatformView()
|
||||
|
||||
val chatRh = remoteHostId.value
|
||||
// We need to have real unreadCount value for displaying it inside top right button
|
||||
// Having activeChat reloaded on every change in it is inefficient (UI lags)
|
||||
val unreadCount = remember {
|
||||
derivedStateOf {
|
||||
chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }?.chatStats?.unreadCount ?: 0
|
||||
chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }?.chatStats?.unreadCount ?: 0
|
||||
}
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
when (chat.chatInfo) {
|
||||
when (chatInfo) {
|
||||
is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> {
|
||||
val perChatTheme = remember(chat.chatInfo, CurrentColors.value.base) { if (chat.chatInfo is ChatInfo.Direct) chat.chatInfo.contact.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else if (chat.chatInfo is ChatInfo.Group) chat.chatInfo.groupInfo.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else null }
|
||||
val perChatTheme = remember(chatInfo, CurrentColors.value.base) { if (chatInfo is ChatInfo.Direct) chatInfo.contact.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else null }
|
||||
val overrides = if (perChatTheme != null) ThemeManager.currentColors(null, perChatTheme, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) else null
|
||||
SimpleXThemeOverride(overrides ?: CurrentColors.collectAsState().value) {
|
||||
ChatLayout(
|
||||
chat,
|
||||
remoteHostId = remoteHostId,
|
||||
chatInfo = activeChatInfo,
|
||||
unreadCount,
|
||||
composeState,
|
||||
composeView = {
|
||||
@@ -131,10 +122,10 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (
|
||||
chat.chatInfo is ChatInfo.Direct
|
||||
&& !chat.chatInfo.contact.sndReady
|
||||
&& chat.chatInfo.contact.active
|
||||
&& !chat.chatInfo.contact.nextSendGrpInv
|
||||
chatInfo is ChatInfo.Direct
|
||||
&& !chatInfo.contact.sndReady
|
||||
&& chatInfo.contact.active
|
||||
&& !chatInfo.contact.nextSendGrpInv
|
||||
) {
|
||||
Text(
|
||||
generalGetString(MR.strings.contact_connection_pending),
|
||||
@@ -144,7 +135,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
)
|
||||
}
|
||||
ComposeView(
|
||||
chatModel, chat, composeState, attachmentOption,
|
||||
chatModel, Chat(remoteHostId = chatRh, chatInfo = chatInfo, chatItems = emptyList()), composeState, attachmentOption,
|
||||
showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } }
|
||||
)
|
||||
}
|
||||
@@ -172,36 +163,38 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
var preloadedContactInfo: Pair<ConnectionStats?, Profile?>? = null
|
||||
var preloadedCode: String? = null
|
||||
var preloadedLink: Pair<String, GroupMemberRole>? = null
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||
preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||
} else if (chat.chatInfo is ChatInfo.Group) {
|
||||
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
|
||||
preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
|
||||
if (chatInfo is ChatInfo.Direct) {
|
||||
preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId)
|
||||
preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second
|
||||
} else if (chatInfo is ChatInfo.Group) {
|
||||
setGroupMembers(chatRh, chatInfo.groupInfo, chatModel)
|
||||
preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chatInfo.groupInfo.groupId)
|
||||
}
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
val chat = remember { activeChat }.value
|
||||
if (chat?.chatInfo is ChatInfo.Direct) {
|
||||
val chatInfo = remember { activeChatInfo }.value
|
||||
if (chatInfo is ChatInfo.Direct) {
|
||||
var contactInfo: Pair<ConnectionStats?, Profile?>? by remember { mutableStateOf(preloadedContactInfo) }
|
||||
var code: String? by remember { mutableStateOf(preloadedCode) }
|
||||
KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) {
|
||||
contactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||
KeyChangeEffect(chatInfo.id, ChatModel.networkStatuses.toMap()) {
|
||||
contactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId)
|
||||
preloadedContactInfo = contactInfo
|
||||
code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||
code = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second
|
||||
preloadedCode = code
|
||||
}
|
||||
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
||||
} else if (chat?.chatInfo is ChatInfo.Group) {
|
||||
var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) }
|
||||
KeyChangeEffect(chat.id) {
|
||||
setGroupMembers(chatRh, (chat.chatInfo as ChatInfo.Group).groupInfo, chatModel)
|
||||
link = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
|
||||
ChatInfoView(chatModel, chatInfo.contact, contactInfo?.first, contactInfo?.second, chatInfo.localAlias, code, close) {
|
||||
showSearch.value = true
|
||||
}
|
||||
} else if (chatInfo is ChatInfo.Group) {
|
||||
var link: Pair<String, GroupMemberRole>? by remember(chatInfo.id) { mutableStateOf(preloadedLink) }
|
||||
KeyChangeEffect(chatInfo.id) {
|
||||
setGroupMembers(chatRh, chatInfo.groupInfo, chatModel)
|
||||
link = chatModel.controller.apiGetGroupLink(chatRh, chatInfo.groupInfo.groupId)
|
||||
preloadedLink = link
|
||||
}
|
||||
GroupChatInfoView(chatModel, chatRh, chat.id, link?.first, link?.second, {
|
||||
GroupChatInfoView(chatModel, chatRh, chatInfo.id, link?.first, link?.second, {
|
||||
link = it
|
||||
preloadedLink = it
|
||||
}, close)
|
||||
}, close, { showSearch.value = true })
|
||||
} else {
|
||||
LaunchedEffect(Unit) {
|
||||
close()
|
||||
@@ -231,7 +224,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
}
|
||||
},
|
||||
loadPrevMessages = {
|
||||
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
|
||||
if (chatModel.chatId.value != activeChatInfo.value.id) return@ChatLayout
|
||||
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout)
|
||||
val firstId = chatModel.chatItems.value.firstOrNull()?.id
|
||||
if (c != null && firstId != null) {
|
||||
@@ -242,56 +235,55 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
},
|
||||
deleteMessage = { itemId, mode ->
|
||||
withBGApi {
|
||||
val cInfo = chat.chatInfo
|
||||
val cInfo = chatInfo
|
||||
val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId }
|
||||
val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo)
|
||||
val toModerate = toDeleteItem?.memberToModerate(chatInfo)
|
||||
val groupInfo = toModerate?.first
|
||||
val groupMember = toModerate?.second
|
||||
val deletedChatItem: ChatItem?
|
||||
val toChatItem: ChatItem?
|
||||
if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) {
|
||||
val r = chatModel.controller.apiDeleteMemberChatItem(
|
||||
val r = if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) {
|
||||
chatModel.controller.apiDeleteMemberChatItems(
|
||||
chatRh,
|
||||
groupId = groupInfo.groupId,
|
||||
groupMemberId = groupMember.groupMemberId,
|
||||
itemId = itemId
|
||||
itemIds = listOf(itemId)
|
||||
)
|
||||
deletedChatItem = r?.first
|
||||
toChatItem = r?.second
|
||||
} else {
|
||||
val r = chatModel.controller.apiDeleteChatItem(
|
||||
chatModel.controller.apiDeleteChatItems(
|
||||
chatRh,
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
itemId = itemId,
|
||||
itemIds = listOf(itemId),
|
||||
mode = mode
|
||||
)
|
||||
deletedChatItem = r?.deletedChatItem?.chatItem
|
||||
toChatItem = r?.toChatItem?.chatItem
|
||||
}
|
||||
if (toChatItem == null && deletedChatItem != null) {
|
||||
chatModel.removeChatItem(chatRh, cInfo, deletedChatItem)
|
||||
} else if (toChatItem != null) {
|
||||
chatModel.upsertChatItem(chatRh, cInfo, toChatItem)
|
||||
val deleted = r?.firstOrNull()
|
||||
if (deleted != null) {
|
||||
deletedChatItem = deleted.deletedChatItem.chatItem
|
||||
toChatItem = deleted.toChatItem?.chatItem
|
||||
withChats {
|
||||
if (toChatItem != null) {
|
||||
upsertChatItem(chatRh, cInfo, toChatItem)
|
||||
} else {
|
||||
removeChatItem(chatRh, cInfo, deletedChatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteMessages = { itemIds ->
|
||||
if (itemIds.isNotEmpty()) {
|
||||
val chatInfo = chat.chatInfo
|
||||
withBGApi {
|
||||
val deletedItems: ArrayList<ChatItem> = arrayListOf()
|
||||
for (itemId in itemIds) {
|
||||
val di = chatModel.controller.apiDeleteChatItem(
|
||||
chatRh, chatInfo.chatType, chatInfo.apiId, itemId, CIDeleteMode.cidmInternal
|
||||
)?.deletedChatItem?.chatItem
|
||||
if (di != null) {
|
||||
deletedItems.add(di)
|
||||
val deleted = chatModel.controller.apiDeleteChatItems(
|
||||
chatRh, chatInfo.chatType, chatInfo.apiId, itemIds, CIDeleteMode.cidmInternal
|
||||
)
|
||||
if (deleted != null) {
|
||||
withChats {
|
||||
for (di in deleted) {
|
||||
removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (di in deletedItems) {
|
||||
chatModel.removeChatItem(chatRh, chatInfo, di)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -307,18 +299,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
onComplete.invoke()
|
||||
}
|
||||
},
|
||||
startCall = out@{ media ->
|
||||
withBGApi {
|
||||
val cInfo = chat.chatInfo
|
||||
if (cInfo is ChatInfo.Direct) {
|
||||
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId)
|
||||
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
|
||||
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
||||
chatModel.showCallView.value = true
|
||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||
}
|
||||
}
|
||||
},
|
||||
startCall = out@{ media -> startChatCall(chatRh, chatInfo, media) },
|
||||
endCall = {
|
||||
val call = chatModel.activeCall.value
|
||||
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
|
||||
@@ -341,7 +322,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
}
|
||||
},
|
||||
openDirectChat = { contactId ->
|
||||
withBGApi {
|
||||
scope.launch {
|
||||
openDirectChat(chatRh, contactId, chatModel)
|
||||
}
|
||||
},
|
||||
@@ -351,11 +332,13 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
},
|
||||
updateContactStats = { contact ->
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||
val r = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId)
|
||||
if (r != null) {
|
||||
val contactStats = r.first
|
||||
if (contactStats != null)
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, contactStats)
|
||||
withChats {
|
||||
updateContactConnectionStats(chatRh, contact, contactStats)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -365,7 +348,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
if (r != null) {
|
||||
val memStats = r.second
|
||||
if (memStats != null) {
|
||||
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats)
|
||||
withChats {
|
||||
updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,7 +359,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
withBGApi {
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
||||
withChats {
|
||||
updateContactConnectionStats(chatRh, contact, cStats)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -382,7 +369,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false)
|
||||
if (r != null) {
|
||||
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second)
|
||||
withChats {
|
||||
updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -403,7 +392,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
reaction = reaction
|
||||
)
|
||||
if (updatedCI != null) {
|
||||
chatModel.updateChatItem(cInfo, updatedCI)
|
||||
withChats {
|
||||
updateChatItem(cInfo, updatedCI)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -411,8 +402,8 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
suspend fun loadChatItemInfo(): ChatItemInfo? {
|
||||
val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id)
|
||||
if (ciInfo != null) {
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
|
||||
if (chatInfo is ChatInfo.Group) {
|
||||
setGroupMembers(chatRh, chatInfo.groupInfo, chatModel)
|
||||
}
|
||||
}
|
||||
return ciInfo
|
||||
@@ -447,42 +438,26 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
}
|
||||
}
|
||||
},
|
||||
addMembers = { groupInfo ->
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
setGroupMembers(chatRh, groupInfo, chatModel)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
AddGroupMembersView(chatRh, groupInfo, false, chatModel, close)
|
||||
}
|
||||
}
|
||||
},
|
||||
openGroupLink = { groupInfo ->
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
val link = chatModel.controller.apiGetGroupLink(chatRh, groupInfo.groupId)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) {
|
||||
GroupLinkView(chatModel, chatRh, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null)
|
||||
}
|
||||
}
|
||||
},
|
||||
addMembers = { groupInfo -> addGroupMembers(view = view, groupInfo = groupInfo, rhId = chatRh, close = { ModalManager.end.closeModals() }) },
|
||||
openGroupLink = { groupInfo -> openGroupLink(view = view, groupInfo = groupInfo, rhId = chatRh, close = { ModalManager.end.closeModals() }) },
|
||||
markRead = { range, unreadCountAfter ->
|
||||
chatModel.markChatItemsRead(chat, range, unreadCountAfter)
|
||||
ntfManager.cancelNotificationsForChat(chat.id)
|
||||
withBGApi {
|
||||
chatModel.controller.apiChatRead(
|
||||
chatRh,
|
||||
chat.chatInfo.chatType,
|
||||
chat.chatInfo.apiId,
|
||||
range
|
||||
)
|
||||
withChats {
|
||||
markChatItemsRead(chatRh, chatInfo, range, unreadCountAfter)
|
||||
ntfManager.cancelNotificationsForChat(chatInfo.id)
|
||||
chatModel.controller.apiChatRead(
|
||||
chatRh,
|
||||
chatInfo.chatType,
|
||||
chatInfo.apiId,
|
||||
range
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
changeNtfsState = { enabled, currentValue -> toggleNotifications(chat, enabled, chatModel, currentValue) },
|
||||
changeNtfsState = { enabled, currentValue -> toggleNotifications(chatRh, chatInfo, enabled, chatModel, currentValue) },
|
||||
onSearchValueChanged = { value ->
|
||||
if (searchText.value == value) return@ChatLayout
|
||||
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
|
||||
if (chatModel.chatId.value != activeChatInfo.value.id) return@ChatLayout
|
||||
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) ?: return@ChatLayout
|
||||
withBGApi {
|
||||
apiFindMessages(c, chatModel, value)
|
||||
@@ -492,27 +467,42 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
onComposed,
|
||||
developerTools = chatModel.controller.appPrefs.developerTools.get(),
|
||||
showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(),
|
||||
showSearch = showSearch
|
||||
)
|
||||
if (appPlatform.isAndroid) {
|
||||
val backgroundColor = MaterialTheme.colors.background
|
||||
val backgroundColorState = rememberUpdatedState(backgroundColor)
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { ModalManager.center.modalCount.value > 0 }
|
||||
.collect { modalBackground ->
|
||||
if (modalBackground) {
|
||||
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false)
|
||||
} else {
|
||||
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, backgroundColorState.value, true, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is ChatInfo.ContactConnection -> {
|
||||
val close = { chatModel.chatId.value = null }
|
||||
ModalView(close, showClose = appPlatform.isAndroid, content = {
|
||||
ContactConnectionInfoView(chatModel, chat.remoteHostId, chat.chatInfo.contactConnection.connReqInv, chat.chatInfo.contactConnection, false, close)
|
||||
ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, false, close)
|
||||
})
|
||||
LaunchedEffect(chat.id) {
|
||||
onComposed(chat.id)
|
||||
LaunchedEffect(chatInfo.id) {
|
||||
onComposed(chatInfo.id)
|
||||
ModalManager.end.closeModals()
|
||||
chatModel.chatItems.clear()
|
||||
}
|
||||
}
|
||||
is ChatInfo.InvalidJSON -> {
|
||||
val close = { chatModel.chatId.value = null }
|
||||
ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chat.chatInfo.json) } }, content = {
|
||||
InvalidJSONView(chat.chatInfo.json)
|
||||
ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chatInfo.json) } }, content = {
|
||||
InvalidJSONView(chatInfo.json)
|
||||
})
|
||||
LaunchedEffect(chat.id) {
|
||||
onComposed(chat.id)
|
||||
LaunchedEffect(chatInfo.id) {
|
||||
onComposed(chatInfo.id)
|
||||
ModalManager.end.closeModals()
|
||||
chatModel.chatItems.clear()
|
||||
}
|
||||
@@ -522,9 +512,22 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
}
|
||||
}
|
||||
|
||||
fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType) {
|
||||
withBGApi {
|
||||
if (chatInfo is ChatInfo.Direct) {
|
||||
val contactInfo = chatModel.controller.apiContactInfo(remoteHostId, chatInfo.contact.contactId)
|
||||
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
|
||||
chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
||||
chatModel.showCallView.value = true
|
||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatLayout(
|
||||
chat: Chat,
|
||||
remoteHostId: State<Long?>,
|
||||
chatInfo: State<ChatInfo>,
|
||||
unreadCount: State<Int>,
|
||||
composeState: MutableState<ComposeState>,
|
||||
composeView: (@Composable () -> Unit),
|
||||
@@ -563,15 +566,17 @@ fun ChatLayout(
|
||||
onSearchValueChanged: (String) -> Unit,
|
||||
onComposed: suspend (chatId: String) -> Unit,
|
||||
developerTools: Boolean,
|
||||
showViaProxy: Boolean
|
||||
showViaProxy: Boolean,
|
||||
showSearch: MutableState<Boolean>
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val attachmentDisabled = remember { derivedStateOf { composeState.value.attachmentDisabled } }
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.desktopOnExternalDrag(
|
||||
enabled = !attachmentDisabled.value && rememberUpdatedState(chat.userCanSend).value,
|
||||
enabled = !attachmentDisabled.value && rememberUpdatedState(chatInfo.value).value.userCanSend,
|
||||
onFiles = { paths -> composeState.onFilesAttached(paths.map { it.toURI() }) },
|
||||
onImage = {
|
||||
// TODO: file is not saved anywhere?!
|
||||
@@ -606,7 +611,7 @@ fun ChatLayout(
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { ChatInfoToolbar(chat, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged) },
|
||||
topBar = { ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) },
|
||||
bottomBar = composeView,
|
||||
modifier = Modifier.navigationBarsWithImePadding(),
|
||||
floatingActionButton = { floatingButton.value() },
|
||||
@@ -628,7 +633,7 @@ fun ChatLayout(
|
||||
.padding(contentPadding)
|
||||
) {
|
||||
ChatItemsList(
|
||||
chat, unreadCount, composeState, searchValue,
|
||||
remoteHostId, chatInfo, unreadCount, composeState, searchValue,
|
||||
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
|
||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||
@@ -643,7 +648,7 @@ fun ChatLayout(
|
||||
|
||||
@Composable
|
||||
fun ChatInfoToolbar(
|
||||
chat: Chat,
|
||||
chatInfo: State<ChatInfo>,
|
||||
back: () -> Unit,
|
||||
info: () -> Unit,
|
||||
startCall: (CallMediaType) -> Unit,
|
||||
@@ -652,35 +657,38 @@ fun ChatInfoToolbar(
|
||||
openGroupLink: (GroupInfo) -> Unit,
|
||||
changeNtfsState: (Boolean, currentValue: MutableState<Boolean>) -> Unit,
|
||||
onSearchValueChanged: (String) -> Unit,
|
||||
showSearch: MutableState<Boolean>
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val showMenu = rememberSaveable { mutableStateOf(false) }
|
||||
var showSearch by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val onBackClicked = {
|
||||
if (!showSearch) {
|
||||
if (!showSearch.value) {
|
||||
back()
|
||||
} else {
|
||||
onSearchValueChanged("")
|
||||
showSearch = false
|
||||
showSearch.value = false
|
||||
}
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
BackHandler(onBack = onBackClicked)
|
||||
}
|
||||
val chatInfo = chatInfo.value
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
val menuItems = arrayListOf<@Composable () -> Unit>()
|
||||
val activeCall by remember { chatModel.activeCall }
|
||||
if (chat.chatInfo is ChatInfo.Local) {
|
||||
if (chatInfo is ChatInfo.Local) {
|
||||
barButtons.add {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
showSearch = true
|
||||
}, enabled = chat.chatInfo.noteFolder.ready
|
||||
IconButton(
|
||||
{
|
||||
showMenu.value = false
|
||||
showSearch.value = true
|
||||
}, enabled = chatInfo.noteFolder.ready
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_search),
|
||||
stringResource(MR.strings.search_verb).capitalize(Locale.current),
|
||||
tint = if (chat.chatInfo.noteFolder.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
tint = if (chatInfo.noteFolder.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -688,41 +696,41 @@ fun ChatInfoToolbar(
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = {
|
||||
showMenu.value = false
|
||||
showSearch = true
|
||||
showSearch.value = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.mergedPreferences.calls.enabled.forUser) {
|
||||
if (chatInfo is ChatInfo.Direct && chatInfo.contact.mergedPreferences.calls.enabled.forUser) {
|
||||
if (activeCall == null) {
|
||||
barButtons.add {
|
||||
if (appPlatform.isAndroid) {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Audio)
|
||||
}, enabled = chat.chatInfo.contact.ready && chat.chatInfo.contact.active
|
||||
}, enabled = chatInfo.contact.ready && chatInfo.contact.active
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_call_500),
|
||||
stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current),
|
||||
tint = if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
tint = if (chatInfo.contact.ready && chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
} else {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Video)
|
||||
}, enabled = chat.chatInfo.contact.ready && chat.chatInfo.contact.active
|
||||
}, enabled = chatInfo.contact.ready && chatInfo.contact.active
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_videocam),
|
||||
stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current),
|
||||
tint = if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
tint = if (chatInfo.contact.ready && chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (activeCall?.contact?.id == chat.id && appPlatform.isDesktop) {
|
||||
} else if (activeCall?.contact?.id == chatInfo.id && appPlatform.isDesktop) {
|
||||
barButtons.add {
|
||||
val call = remember { chatModel.activeCall }.value
|
||||
val connectedAt = call?.connectedAt
|
||||
@@ -751,7 +759,7 @@ fun ChatInfoToolbar(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active && activeCall == null) {
|
||||
if (chatInfo.contact.ready && chatInfo.contact.active && activeCall == null) {
|
||||
menuItems.add {
|
||||
if (appPlatform.isAndroid) {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
|
||||
@@ -766,12 +774,12 @@ fun ChatInfoToolbar(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers) {
|
||||
if (!chat.chatInfo.incognito) {
|
||||
} else if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canAddMembers) {
|
||||
if (!chatInfo.incognito) {
|
||||
barButtons.add {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
addMembers(chat.chatInfo.groupInfo)
|
||||
addMembers(chatInfo.groupInfo)
|
||||
}) {
|
||||
Icon(painterResource(MR.images.ic_person_add_500), stringResource(MR.strings.icon_descr_add_members), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
@@ -780,15 +788,16 @@ fun ChatInfoToolbar(
|
||||
barButtons.add {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
openGroupLink(chat.chatInfo.groupInfo)
|
||||
openGroupLink(chatInfo.groupInfo)
|
||||
}) {
|
||||
Icon(painterResource(MR.images.ic_add_link), stringResource(MR.strings.group_link), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.ready && chat.chatInfo.contact.active) || chat.chatInfo is ChatInfo.Group) {
|
||||
val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) }
|
||||
|
||||
if ((chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) || chatInfo is ChatInfo.Group) {
|
||||
val ntfsEnabled = remember { mutableStateOf(chatInfo.ntfsEnabled) }
|
||||
menuItems.add {
|
||||
ItemAction(
|
||||
if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat),
|
||||
@@ -814,10 +823,10 @@ fun ChatInfoToolbar(
|
||||
}
|
||||
|
||||
DefaultTopAppBar(
|
||||
navigationButton = { if (appPlatform.isAndroid || showSearch) { NavigationButtonBack(onBackClicked) } },
|
||||
title = { ChatInfoToolbarTitle(chat.chatInfo) },
|
||||
onTitleClick = if (chat.chatInfo is ChatInfo.Local) null else info,
|
||||
showSearch = showSearch,
|
||||
navigationButton = { if (appPlatform.isAndroid || showSearch.value) { NavigationButtonBack(onBackClicked) } },
|
||||
title = { ChatInfoToolbarTitle(chatInfo) },
|
||||
onTitleClick = if (chatInfo is ChatInfo.Local) null else info,
|
||||
showSearch = showSearch.value,
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
buttons = barButtons
|
||||
)
|
||||
@@ -883,7 +892,8 @@ val CIListStateSaver = run {
|
||||
|
||||
@Composable
|
||||
fun BoxWithConstraintsScope.ChatItemsList(
|
||||
chat: Chat,
|
||||
remoteHostId: State<Long?>,
|
||||
chatInfo: State<ChatInfo>,
|
||||
unreadCount: State<Int>,
|
||||
composeState: MutableState<ComposeState>,
|
||||
searchValue: State<String>,
|
||||
@@ -916,7 +926,9 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
ScrollToBottom(chat.id, listState, chatModel.chatItems)
|
||||
val remoteHostId = remember { remoteHostId }.value
|
||||
val chatInfo = remember { chatInfo }.value
|
||||
ScrollToBottom(chatInfo.id, listState, chatModel.chatItems)
|
||||
var prevSearchEmptiness by rememberSaveable { mutableStateOf(searchValue.value.isEmpty()) }
|
||||
// Scroll to bottom when search value changes from something to nothing and back
|
||||
LaunchedEffect(searchValue.value.isEmpty()) {
|
||||
@@ -941,13 +953,13 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.lastIndex, index + 1), -maxHeightRounded) }
|
||||
}
|
||||
}
|
||||
LaunchedEffect(chat.id) {
|
||||
LaunchedEffect(chatInfo.id) {
|
||||
var stopListening = false
|
||||
snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastIndex }
|
||||
.distinctUntilChanged()
|
||||
.filter { !stopListening }
|
||||
.collect {
|
||||
onComposed(chat.id)
|
||||
onComposed(chatInfo.id)
|
||||
stopListening = true
|
||||
}
|
||||
}
|
||||
@@ -966,7 +978,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
|
||||
if (it == DismissValue.DismissedToStart) {
|
||||
scope.launch {
|
||||
if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chat.chatInfo !is ChatInfo.Local) {
|
||||
if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local) {
|
||||
if (composeState.value.editing) {
|
||||
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
||||
} else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||
@@ -1000,14 +1012,14 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
tryOrShowError("${cItem.id}ChatItem", error = {
|
||||
CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
|
||||
}) {
|
||||
ChatItemView(chat.remoteHostId, chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy)
|
||||
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?) {
|
||||
val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
if (chatInfo is ChatInfo.Group) {
|
||||
if (cItem.chatDir is CIDirection.GroupRcv) {
|
||||
val member = cItem.chatDir.groupMember
|
||||
val (prevMember, memCount) =
|
||||
@@ -1047,7 +1059,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
swipeableModifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Box(Modifier.clickable { showMemberInfo(chat.chatInfo.groupInfo, member) }) {
|
||||
Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) {
|
||||
MemberImage(member)
|
||||
}
|
||||
ChatItemViewShortHand(cItem, range)
|
||||
@@ -1101,7 +1113,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
}
|
||||
|
||||
if (cItem.isRcvNew && chat.id == ChatModel.chatId.value) {
|
||||
if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) {
|
||||
LaunchedEffect(cItem.id) {
|
||||
scope.launch {
|
||||
delay(600)
|
||||
@@ -1112,7 +1124,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
}
|
||||
}
|
||||
FloatingButtons(chatModel.chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState)
|
||||
FloatingButtons(chatModel.chatItems, unreadCount, remoteHostId, chatInfo, searchValue, markRead, setFloatingButton, listState)
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { listState.isScrollInProgress }
|
||||
.collect {
|
||||
@@ -1145,7 +1157,7 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
|
||||
.filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it }
|
||||
.collect {
|
||||
try {
|
||||
if (listState.firstVisibleItemIndex == 0 || (listState.firstVisibleItemIndex == 1 && listState.layoutInfo.totalItemsCount == chatItems.size)) {
|
||||
if (listState.firstVisibleItemIndex == 0 || (listState.firstVisibleItemIndex == 1 && listState.layoutInfo.totalItemsCount == chatItems.value.size)) {
|
||||
if (appPlatform.isAndroid) listState.animateScrollToItem(0) else listState.scrollToItem(0)
|
||||
} else {
|
||||
if (appPlatform.isAndroid) listState.animateScrollBy(scrollDistance) else listState.scrollBy(scrollDistance)
|
||||
@@ -1156,6 +1168,8 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
|
||||
* this coroutine will be canceled with the message "Current mutation had a higher priority" because of animatedScroll.
|
||||
* Which breaks auto-scrolling to bottom. So just ignoring the exception
|
||||
* */
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "Failed to scroll: ${e.stackTraceToString()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1165,7 +1179,8 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
|
||||
fun BoxWithConstraintsScope.FloatingButtons(
|
||||
chatItems: State<List<ChatItem>>,
|
||||
unreadCount: State<Int>,
|
||||
minUnreadItemId: Long,
|
||||
remoteHostId: Long?,
|
||||
chatInfo: ChatInfo,
|
||||
searchValue: State<String>,
|
||||
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
||||
setFloatingButton: (@Composable () -> Unit) -> Unit,
|
||||
@@ -1246,8 +1261,9 @@ fun BoxWithConstraintsScope.FloatingButtons(
|
||||
generalGetString(MR.strings.mark_read),
|
||||
painterResource(MR.images.ic_check),
|
||||
onClick = {
|
||||
val minUnreadItemId = chatModel.chats.value.firstOrNull { it.remoteHostId == remoteHostId && it.id == chatInfo.id }?.chatStats?.minUnreadItemId ?: return@ItemAction
|
||||
markRead(
|
||||
CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
|
||||
CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.value.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
|
||||
bottomUnreadCount
|
||||
)
|
||||
showDropDown.value = false
|
||||
@@ -1316,7 +1332,7 @@ private fun TopEndFloatingButton(
|
||||
val interactionSource = interactionSourceWithDetection(onClick, onLongClick)
|
||||
FloatingActionButton(
|
||||
{}, // no action here
|
||||
modifier.size(48.dp),
|
||||
modifier.size(48.dp).onRightClick(onLongClick),
|
||||
backgroundColor = MaterialTheme.colors.secondaryVariant,
|
||||
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp),
|
||||
interactionSource = interactionSource,
|
||||
@@ -1334,6 +1350,28 @@ private fun TopEndFloatingButton(
|
||||
|
||||
val chatViewScrollState = MutableStateFlow(false)
|
||||
|
||||
fun addGroupMembers(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (() -> Unit)? = null) {
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
setGroupMembers(rhId, groupInfo, chatModel)
|
||||
close?.invoke()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
AddGroupMembersView(rhId, groupInfo, false, chatModel, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openGroupLink(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (() -> Unit)? = null) {
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
val link = chatModel.controller.apiGetGroupLink(rhId, groupInfo.groupId)
|
||||
close?.invoke()
|
||||
ModalManager.end.showModalCloseable(true) {
|
||||
GroupLinkView(chatModel, rhId, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bottomEndFloatingButton(
|
||||
unreadCount: Int,
|
||||
showButtonWithCounter: Boolean,
|
||||
@@ -1378,8 +1416,8 @@ private fun bottomEndFloatingButton(
|
||||
}
|
||||
}
|
||||
|
||||
private fun markUnreadChatAsRead(activeChat: MutableState<Chat?>, chatModel: ChatModel) {
|
||||
val chat = activeChat.value
|
||||
private fun markUnreadChatAsRead(chatId: String) {
|
||||
val chat = chatModel.chats.value.firstOrNull { it.id == chatId }
|
||||
if (chat?.chatStats?.unreadChat != true) return
|
||||
withApi {
|
||||
val chatRh = chat.remoteHostId
|
||||
@@ -1389,9 +1427,10 @@ private fun markUnreadChatAsRead(activeChat: MutableState<Chat?>, chatModel: Cha
|
||||
chat.chatInfo.apiId,
|
||||
false
|
||||
)
|
||||
if (success && chat.id == activeChat.value?.id) {
|
||||
activeChat.value = chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))
|
||||
chatModel.replaceChat(chatRh, chat.id, activeChat.value!!)
|
||||
if (success) {
|
||||
withChats {
|
||||
replaceChat(chatRh, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1544,12 +1583,8 @@ fun PreviewChatLayout() {
|
||||
val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) }
|
||||
val searchValue = remember { mutableStateOf("") }
|
||||
ChatLayout(
|
||||
chat = Chat(
|
||||
remoteHostId = null,
|
||||
chatInfo = ChatInfo.Direct.sampleData,
|
||||
chatItems = chatItems,
|
||||
chatStats = Chat.ChatStats()
|
||||
),
|
||||
remoteHostId = remember { mutableStateOf(null) },
|
||||
chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) },
|
||||
unreadCount = unreadCount,
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
composeView = {},
|
||||
@@ -1589,6 +1624,7 @@ fun PreviewChatLayout() {
|
||||
onComposed = {},
|
||||
developerTools = false,
|
||||
showViaProxy = false,
|
||||
showSearch = remember { mutableStateOf(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1618,12 +1654,8 @@ fun PreviewGroupChatLayout() {
|
||||
val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) }
|
||||
val searchValue = remember { mutableStateOf("") }
|
||||
ChatLayout(
|
||||
chat = Chat(
|
||||
remoteHostId = null,
|
||||
chatInfo = ChatInfo.Group.sampleData,
|
||||
chatItems = chatItems,
|
||||
chatStats = Chat.ChatStats()
|
||||
),
|
||||
remoteHostId = remember { mutableStateOf(null) },
|
||||
chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) },
|
||||
unreadCount = unreadCount,
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
composeView = {},
|
||||
@@ -1663,6 +1695,7 @@ fun PreviewGroupChatLayout() {
|
||||
onComposed = {},
|
||||
developerTools = false,
|
||||
showViaProxy = false,
|
||||
showSearch = remember { mutableStateOf(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+18
-8
@@ -22,6 +22,7 @@ import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.filesToDelete
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.item.*
|
||||
@@ -393,7 +394,9 @@ fun ComposeView(
|
||||
ttl = ttl
|
||||
)
|
||||
if (aChatItem != null) {
|
||||
chatModel.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem)
|
||||
withChats {
|
||||
addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem)
|
||||
}
|
||||
return aChatItem.chatItem
|
||||
}
|
||||
if (file != null) removeFile(file.filePath)
|
||||
@@ -421,7 +424,9 @@ fun ComposeView(
|
||||
ttl = ttl
|
||||
)
|
||||
if (chatItem != null) {
|
||||
chatModel.addChatItem(rhId, chat.chatInfo, chatItem)
|
||||
withChats {
|
||||
addChatItem(rhId, chat.chatInfo, chatItem)
|
||||
}
|
||||
}
|
||||
return chatItem
|
||||
}
|
||||
@@ -458,7 +463,9 @@ fun ComposeView(
|
||||
val mc = checkLinkPreview()
|
||||
val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc)
|
||||
if (contact != null) {
|
||||
chatModel.updateContact(chat.remoteHostId, contact)
|
||||
withChats {
|
||||
updateContact(chat.remoteHostId, contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,7 +481,9 @@ fun ComposeView(
|
||||
mc = updateMsgContent(oldMsgContent),
|
||||
live = live
|
||||
)
|
||||
if (updatedItem != null) chatModel.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem)
|
||||
if (updatedItem != null) withChats {
|
||||
upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem)
|
||||
}
|
||||
return updatedItem?.chatItem
|
||||
}
|
||||
return null
|
||||
@@ -827,7 +836,7 @@ fun ComposeView(
|
||||
chatModel.sharedContent.value = null
|
||||
}
|
||||
|
||||
val userCanSend = rememberUpdatedState(chat.userCanSend)
|
||||
val userCanSend = rememberUpdatedState(chat.chatInfo.userCanSend)
|
||||
val sendMsgEnabled = rememberUpdatedState(chat.chatInfo.sendMsgEnabled)
|
||||
val userIsObserver = rememberUpdatedState(chat.userIsObserver)
|
||||
val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv)
|
||||
@@ -863,6 +872,7 @@ fun ComposeView(
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
Row(
|
||||
modifier = Modifier.background(MaterialTheme.colors.background).padding(end = 8.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
@@ -886,7 +896,7 @@ fun ComposeView(
|
||||
&& !nextSendGrpInv.value
|
||||
IconButton(
|
||||
attachmentClicked,
|
||||
Modifier.padding(bottom = if (appPlatform.isAndroid) 0.dp else with(LocalDensity.current) { 7.sp.toDp() }),
|
||||
Modifier.padding(bottom = if (appPlatform.isAndroid) 2.dp else with(LocalDensity.current) { 7.sp.toDp() }),
|
||||
enabled = attachmentEnabled
|
||||
) {
|
||||
Icon(
|
||||
@@ -927,8 +937,8 @@ fun ComposeView(
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(rememberUpdatedState(chat.userCanSend).value) {
|
||||
if (!chat.userCanSend) {
|
||||
LaunchedEffect(rememberUpdatedState(chat.chatInfo.userCanSend).value) {
|
||||
if (!chat.chatInfo.userCanSend) {
|
||||
clearCurrentDraft()
|
||||
clearState()
|
||||
}
|
||||
|
||||
+5
-2
@@ -19,6 +19,7 @@ import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.PreferenceToggle
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@@ -40,8 +41,10 @@ fun ContactPreferencesView(
|
||||
val prefs = contactFeaturesAllowedToPrefs(featuresAllowed)
|
||||
val toContact = m.controller.apiSetContactPrefs(rhId, ct.contactId, prefs)
|
||||
if (toContact != null) {
|
||||
m.updateContact(rhId, toContact)
|
||||
currentFeaturesAllowed = featuresAllowed
|
||||
withChats {
|
||||
updateContact(rhId, toContact)
|
||||
currentFeaturesAllowed = featuresAllowed
|
||||
}
|
||||
}
|
||||
afterSave()
|
||||
}
|
||||
|
||||
+5
-2
@@ -24,6 +24,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.ChatInfoToolbarTitle
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -58,7 +59,9 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea
|
||||
for (contactId in selectedContacts) {
|
||||
val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value)
|
||||
if (member != null) {
|
||||
chatModel.upsertGroupMember(rhId, groupInfo, member)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, groupInfo, member)
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
@@ -81,7 +84,7 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> {
|
||||
val memberContactIds = chatModel.groupMembers
|
||||
.filter { it.memberCurrent }
|
||||
.mapNotNull { it.memberContactId }
|
||||
return chatModel.chats
|
||||
return chatModel.chats.value
|
||||
.asSequence()
|
||||
.map { it.chatInfo }
|
||||
.filterIsInstance<ChatInfo.Direct>()
|
||||
|
||||
+90
-13
@@ -26,6 +26,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
@@ -40,10 +41,10 @@ import kotlinx.coroutines.launch
|
||||
const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
|
||||
|
||||
@Composable
|
||||
fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit) {
|
||||
fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit, onSearchClicked: () -> Unit) {
|
||||
BackHandler(onBack = close)
|
||||
// TODO derivedStateOf?
|
||||
val chat = chatModel.chats.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId }
|
||||
val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId }
|
||||
val currentUser = chatModel.currentUser.value
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
if (chat != null && chat.chatInfo is ChatInfo.Group && currentUser != null) {
|
||||
@@ -56,7 +57,7 @@ fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLi
|
||||
sendReceipts = sendReceipts,
|
||||
setSendReceipts = { sendRcpts ->
|
||||
val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool)
|
||||
updateChatSettings(chat, chatSettings, chatModel)
|
||||
updateChatSettings(chat.remoteHostId, chat.chatInfo, chatSettings, chatModel)
|
||||
sendReceipts.value = sendRcpts
|
||||
},
|
||||
members = chatModel.groupMembers
|
||||
@@ -113,7 +114,8 @@ fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLi
|
||||
leaveGroup = { leaveGroupDialog(rhId, groupInfo, chatModel, close) },
|
||||
manageGroupLink = {
|
||||
ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) }
|
||||
}
|
||||
},
|
||||
onSearchClicked = onSearchClicked
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -131,13 +133,15 @@ fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, cl
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiDeleteChat(chat.remoteHostId, chatInfo.chatType, chatInfo.apiId)
|
||||
if (r) {
|
||||
chatModel.removeChat(chat.remoteHostId, chatInfo.id)
|
||||
if (chatModel.chatId.value == chatInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
withChats {
|
||||
removeChat(chat.remoteHostId, chatInfo.id)
|
||||
if (chatModel.chatId.value == chatInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
ntfManager.cancelNotificationsForChat(chatInfo.id)
|
||||
close?.invoke()
|
||||
}
|
||||
ntfManager.cancelNotificationsForChat(chatInfo.id)
|
||||
close?.invoke()
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -169,7 +173,9 @@ private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMembe
|
||||
withBGApi {
|
||||
val updatedMember = chatModel.controller.apiRemoveMember(rhId, groupInfo.groupId, mem.groupMemberId)
|
||||
if (updatedMember != null) {
|
||||
chatModel.upsertGroupMember(rhId, groupInfo, updatedMember)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -177,6 +183,57 @@ private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMembe
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchButton(chat: Chat, group: GroupInfo, close: () -> Unit, onSearchClicked: () -> Unit) {
|
||||
val disabled = !group.ready || chat.chatItems.isEmpty()
|
||||
|
||||
InfoViewActionButton(
|
||||
icon = painterResource(MR.images.ic_search),
|
||||
title = generalGetString(MR.strings.info_view_search_button),
|
||||
disabled = disabled,
|
||||
disabledLook = disabled,
|
||||
onClick = {
|
||||
if (appPlatform.isAndroid) {
|
||||
close.invoke()
|
||||
}
|
||||
onSearchClicked()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MuteButton(chat: Chat, groupInfo: GroupInfo) {
|
||||
val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) }
|
||||
|
||||
InfoViewActionButton(
|
||||
icon = if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications),
|
||||
title = if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat),
|
||||
disabled = !groupInfo.ready,
|
||||
disabledLook = !groupInfo.ready,
|
||||
onClick = {
|
||||
toggleNotifications(chat.remoteHostId, chat.chatInfo, !ntfsEnabled.value, chatModel, ntfsEnabled)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddGroupMembersButton(chat: Chat, groupInfo: GroupInfo) {
|
||||
InfoViewActionButton(
|
||||
icon = if (groupInfo.incognito) painterResource(MR.images.ic_add_link) else painterResource(MR.images.ic_person_add_500),
|
||||
title = stringResource(MR.strings.action_button_add_members),
|
||||
disabled = !groupInfo.ready,
|
||||
disabledLook = !groupInfo.ready,
|
||||
onClick = {
|
||||
if (groupInfo.incognito) {
|
||||
openGroupLink(groupInfo = groupInfo, rhId = chat.remoteHostId)
|
||||
} else {
|
||||
addGroupMembers(groupInfo = groupInfo, rhId = chat.remoteHostId)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun GroupChatInfoLayout(
|
||||
chat: Chat,
|
||||
@@ -196,6 +253,8 @@ fun GroupChatInfoLayout(
|
||||
clearChat: () -> Unit,
|
||||
leaveGroup: () -> Unit,
|
||||
manageGroupLink: () -> Unit,
|
||||
close: () -> Unit = { ModalManager.closeAllModalsEverywhere()},
|
||||
onSearchClicked: () -> Unit
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -219,6 +278,24 @@ fun GroupChatInfoLayout(
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
SearchButton(chat, groupInfo, close, onSearchClicked)
|
||||
if (groupInfo.canAddMembers) {
|
||||
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
|
||||
AddGroupMembersButton(chat, groupInfo)
|
||||
}
|
||||
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
|
||||
MuteButton(chat, groupInfo)
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
SectionView {
|
||||
if (groupInfo.canEdit) {
|
||||
EditGroupProfileButton(editGroupProfile)
|
||||
@@ -235,7 +312,7 @@ fun GroupChatInfoLayout(
|
||||
|
||||
WallpaperButton {
|
||||
ModalManager.end.showModal {
|
||||
val chat = remember { derivedStateOf { chatModel.chats.firstOrNull { it.id == chat.id } } }
|
||||
val chat = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chat.id } } }
|
||||
val c = chat.value
|
||||
if (c != null) {
|
||||
ChatWallpaperEditorModal(c)
|
||||
@@ -584,7 +661,7 @@ fun PreviewGroupChatInfoLayout() {
|
||||
members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData),
|
||||
developerTools = false,
|
||||
groupLink = null,
|
||||
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {},
|
||||
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, onSearchClicked = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+114
-41
@@ -29,6 +29,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -52,7 +53,7 @@ fun GroupMemberInfoView(
|
||||
closeAll: () -> Unit, // Close all open windows up to ChatView
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
val chat = chatModel.chats.firstOrNull { ch -> ch.id == chatModel.chatId.value && ch.remoteHostId == rhId }
|
||||
val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatModel.chatId.value && ch.remoteHostId == rhId }
|
||||
val connStats = remember { mutableStateOf(connectionStats) }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
var progressIndicator by remember { mutableStateOf(false) }
|
||||
@@ -72,13 +73,15 @@ fun GroupMemberInfoView(
|
||||
withBGApi {
|
||||
val c = chatModel.controller.apiGetChat(rhId, ChatType.Direct, it)
|
||||
if (c != null) {
|
||||
if (chatModel.getContactChat(it) == null) {
|
||||
chatModel.addChat(c)
|
||||
withChats {
|
||||
if (chatModel.getContactChat(it) == null) {
|
||||
addChat(c)
|
||||
}
|
||||
chatModel.chatItemStatuses.clear()
|
||||
chatModel.chatItems.replaceAll(c.chatItems)
|
||||
chatModel.chatId.value = c.id
|
||||
closeAll()
|
||||
}
|
||||
chatModel.chatItemStatuses.clear()
|
||||
chatModel.chatItems.replaceAll(c.chatItems)
|
||||
chatModel.chatId.value = c.id
|
||||
closeAll()
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -88,8 +91,10 @@ fun GroupMemberInfoView(
|
||||
val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId)
|
||||
if (memberContact != null) {
|
||||
val memberChat = Chat(remoteHostId = rhId, ChatInfo.Direct(memberContact), chatItems = arrayListOf())
|
||||
chatModel.addChat(memberChat)
|
||||
openLoadedChat(memberChat, chatModel)
|
||||
withChats {
|
||||
addChat(memberChat)
|
||||
openLoadedChat(memberChat, chatModel)
|
||||
}
|
||||
closeAll()
|
||||
chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected())
|
||||
}
|
||||
@@ -114,7 +119,9 @@ fun GroupMemberInfoView(
|
||||
withBGApi {
|
||||
kotlin.runCatching {
|
||||
val mem = chatModel.controller.apiMemberRole(rhId, groupInfo.groupId, member.groupMemberId, it)
|
||||
chatModel.upsertGroupMember(rhId, groupInfo, mem)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, groupInfo, mem)
|
||||
}
|
||||
}.onFailure {
|
||||
newRole.value = prevValue
|
||||
}
|
||||
@@ -127,7 +134,9 @@ fun GroupMemberInfoView(
|
||||
val r = chatModel.controller.apiSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId)
|
||||
if (r != null) {
|
||||
connStats.value = r.second
|
||||
chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
withChats {
|
||||
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
@@ -139,7 +148,9 @@ fun GroupMemberInfoView(
|
||||
val r = chatModel.controller.apiAbortSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId)
|
||||
if (r != null) {
|
||||
connStats.value = r.second
|
||||
chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
withChats {
|
||||
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
@@ -150,7 +161,9 @@ fun GroupMemberInfoView(
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false)
|
||||
if (r != null) {
|
||||
connStats.value = r.second
|
||||
chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
withChats {
|
||||
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
@@ -161,7 +174,9 @@ fun GroupMemberInfoView(
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = true)
|
||||
if (r != null) {
|
||||
connStats.value = r.second
|
||||
chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
withChats {
|
||||
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
@@ -177,15 +192,17 @@ fun GroupMemberInfoView(
|
||||
verify = { code ->
|
||||
chatModel.controller.apiVerifyGroupMember(rhId, mem.groupId, mem.groupMemberId, code)?.let { r ->
|
||||
val (verified, existingCode) = r
|
||||
chatModel.upsertGroupMember(
|
||||
rhId,
|
||||
groupInfo,
|
||||
mem.copy(
|
||||
activeConn = mem.activeConn?.copy(
|
||||
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
|
||||
withChats {
|
||||
upsertGroupMember(
|
||||
rhId,
|
||||
groupInfo,
|
||||
mem.copy(
|
||||
activeConn = mem.activeConn?.copy(
|
||||
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
r
|
||||
}
|
||||
},
|
||||
@@ -211,7 +228,9 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c
|
||||
withBGApi {
|
||||
val removedMember = chatModel.controller.apiRemoveMember(rhId, member.groupId, member.groupMemberId)
|
||||
if (removedMember != null) {
|
||||
chatModel.upsertGroupMember(rhId, groupInfo, removedMember)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, groupInfo, removedMember)
|
||||
}
|
||||
}
|
||||
close?.invoke()
|
||||
}
|
||||
@@ -246,10 +265,10 @@ fun GroupMemberInfoLayout(
|
||||
verifyClicked: () -> Unit,
|
||||
) {
|
||||
val cStats = connStats.value
|
||||
fun knownDirectChat(contactId: Long): Chat? {
|
||||
fun knownDirectChat(contactId: Long): Pair<Chat, Contact>? {
|
||||
val chat = getContactChat(contactId)
|
||||
return if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) {
|
||||
chat
|
||||
chat to chat.chatInfo.contact
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -309,17 +328,53 @@ fun GroupMemberInfoLayout(
|
||||
|
||||
val contactId = member.memberContactId
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val knownChat = if (contactId != null) knownDirectChat(contactId) else null
|
||||
if (knownChat != null) {
|
||||
val (chat, contact) = knownChat
|
||||
OpenChatButton(onClick = { openDirectChat(contact.contactId) })
|
||||
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
|
||||
AudioCallButton(chat, contact)
|
||||
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
|
||||
VideoButton(chat, contact)
|
||||
} else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) {
|
||||
if (contactId != null) {
|
||||
OpenChatButton(onClick = { openDirectChat(contactId) }) // legacy - only relevant for direct contacts created when joining group
|
||||
} else {
|
||||
OpenChatButton(onClick = { createMemberContact() })
|
||||
}
|
||||
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
|
||||
InfoViewActionButton(painterResource(MR.images.ic_call), generalGetString(MR.strings.info_view_call_button), disabled = false, disabledLook = true, onClick = {
|
||||
showSendMessageToEnableCallsAlert()
|
||||
})
|
||||
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
|
||||
InfoViewActionButton(painterResource(MR.images.ic_videocam), generalGetString(MR.strings.info_view_video_button), disabled = false, disabledLook = true, onClick = {
|
||||
showSendMessageToEnableCallsAlert()
|
||||
})
|
||||
} else { // no known contact chat && directMessages are off
|
||||
InfoViewActionButton(painterResource(MR.images.ic_chat_bubble), generalGetString(MR.strings.info_view_message_button), disabled = false, disabledLook = true, onClick = {
|
||||
showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_send_message_to_member_alert_title))
|
||||
})
|
||||
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
|
||||
InfoViewActionButton(painterResource(MR.images.ic_call), generalGetString(MR.strings.info_view_call_button), disabled = false, disabledLook = true, onClick = {
|
||||
showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title))
|
||||
})
|
||||
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
|
||||
InfoViewActionButton(painterResource(MR.images.ic_videocam), generalGetString(MR.strings.info_view_video_button), disabled = false, disabledLook = true, onClick = {
|
||||
showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title))
|
||||
})
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
if (member.memberActive) {
|
||||
SectionView {
|
||||
if (contactId != null && knownDirectChat(contactId) != null) {
|
||||
OpenChatButton(onClick = { openDirectChat(contactId) })
|
||||
} else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) {
|
||||
if (contactId != null) {
|
||||
OpenChatButton(onClick = { openDirectChat(contactId) })
|
||||
} else if (member.activeConn?.peerChatVRange?.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) == true) {
|
||||
OpenChatButton(onClick = { createMemberContact() })
|
||||
}
|
||||
}
|
||||
if (connectionCode != null) {
|
||||
VerifyCodeButton(member.verified, verifyClicked)
|
||||
}
|
||||
@@ -420,6 +475,20 @@ fun GroupMemberInfoLayout(
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSendMessageToEnableCallsAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.cant_call_member_alert_title),
|
||||
text = generalGetString(MR.strings.cant_call_member_send_message_alert_text)
|
||||
)
|
||||
}
|
||||
|
||||
private fun showDirectMessagesProhibitedAlert(title: String) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = title,
|
||||
text = generalGetString(MR.strings.direct_messages_are_prohibited_in_chat)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupMemberInfoHeader(member: GroupMember) {
|
||||
Column(
|
||||
@@ -513,12 +582,12 @@ fun RemoveMemberButton(onClick: () -> Unit) {
|
||||
|
||||
@Composable
|
||||
fun OpenChatButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_chat),
|
||||
stringResource(MR.strings.button_send_direct_message),
|
||||
click = onClick,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
InfoViewActionButton(
|
||||
icon = painterResource(MR.images.ic_chat_bubble),
|
||||
title = generalGetString(MR.strings.info_view_message_button),
|
||||
disabled = false,
|
||||
disabledLook = false,
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@@ -621,7 +690,9 @@ fun updateMemberSettings(rhId: Long?, gInfo: GroupInfo, member: GroupMember, mem
|
||||
withBGApi {
|
||||
val success = ChatController.apiSetMemberSettings(rhId, gInfo.groupId, member.groupMemberId, memberSettings)
|
||||
if (success) {
|
||||
ChatModel.upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings))
|
||||
withChats {
|
||||
upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -652,7 +723,9 @@ fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) {
|
||||
fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocked: Boolean) {
|
||||
withBGApi {
|
||||
val updatedMember = ChatController.apiBlockMemberForAll(rhId, gInfo.groupId, member.groupMemberId, blocked)
|
||||
chatModel.upsertGroupMember(rhId, gInfo, updatedMember)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, gInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -17,6 +17,7 @@ import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.PreferenceToggleWithIcon
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
@@ -43,8 +44,10 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () ->
|
||||
val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences())
|
||||
val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp)
|
||||
if (g != null) {
|
||||
m.updateGroup(rhId, g)
|
||||
currentPreferences = preferences
|
||||
withChats {
|
||||
updateGroup(rhId, g)
|
||||
currentPreferences = preferences
|
||||
}
|
||||
}
|
||||
afterSave()
|
||||
}
|
||||
|
||||
+4
-1
@@ -17,6 +17,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.*
|
||||
@@ -38,7 +39,9 @@ fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl
|
||||
withBGApi {
|
||||
val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p)
|
||||
if (gInfo != null) {
|
||||
chatModel.updateGroup(rhId, gInfo)
|
||||
withChats {
|
||||
updateGroup(rhId, gInfo)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -28,6 +28,7 @@ import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.views.chat.item.MarkdownText
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.GroupInfo
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.common.platform.chatJsonLength
|
||||
@@ -52,7 +53,9 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: ()
|
||||
val res = m.controller.apiUpdateGroup(rhId, gInfo.groupId, groupProfileUpdated)
|
||||
if (res != null) {
|
||||
gInfo = res
|
||||
m.updateGroup(rhId, res)
|
||||
withChats {
|
||||
updateGroup(rhId, res)
|
||||
}
|
||||
welcomeText.value = welcome ?: ""
|
||||
}
|
||||
afterSave()
|
||||
|
||||
+2
-1
@@ -209,7 +209,7 @@ fun CIImageView(
|
||||
val loaded = res.value
|
||||
if (loaded != null && file != null) {
|
||||
val (imageBitmap, data, _) = loaded
|
||||
SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, @Composable { painter, onClick -> ImageView(painter, image, file.fileSource, onClick) })
|
||||
SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, smallView, @Composable { painter, onClick -> ImageView(painter, image, file.fileSource, onClick) })
|
||||
} else {
|
||||
imageView(base64ToBitmap(image), onClick = {
|
||||
if (file != null) {
|
||||
@@ -285,5 +285,6 @@ expect fun SimpleAndAnimatedImageView(
|
||||
imageBitmap: ImageBitmap,
|
||||
file: CIFile?,
|
||||
imageProvider: () -> ImageGalleryProvider,
|
||||
smallView: Boolean,
|
||||
ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit
|
||||
)
|
||||
|
||||
+5
@@ -12,8 +12,10 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.CryptoFile
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
import chat.simplex.common.views.chat.ProviderMedia
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
@@ -55,6 +57,9 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
||||
val scope = rememberCoroutineScope()
|
||||
val playersToRelease = rememberSaveable { mutableSetOf<URI>() }
|
||||
DisposableEffectOnGone(
|
||||
always = {
|
||||
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, Color.Black, false, false)
|
||||
},
|
||||
whenGone = { playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) } }
|
||||
)
|
||||
|
||||
|
||||
+89
-53
@@ -1,6 +1,7 @@
|
||||
package chat.simplex.common.views.chatlist
|
||||
|
||||
import SectionItemView
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
@@ -19,6 +20,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
@@ -28,11 +30,11 @@ import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, oneHandUI: State<Boolean>) {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val showMarkRead = remember(chat.chatStats.unreadCount, chat.chatStats.unreadChat) {
|
||||
chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat
|
||||
@@ -47,6 +49,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
val showChatPreviews = chatModel.showChatPreviews.value
|
||||
val inProgress = remember { mutableStateOf(false) }
|
||||
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(inProgress.value) {
|
||||
progressByTimeout = if (inProgress.value) {
|
||||
delay(1000)
|
||||
@@ -56,6 +59,8 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
}
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
when (chat.chatInfo) {
|
||||
is ChatInfo.Direct -> {
|
||||
val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact)
|
||||
@@ -65,7 +70,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, disabled, linkMode, inProgress = false, progressByTimeout = false)
|
||||
}
|
||||
},
|
||||
click = { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) },
|
||||
click = { scope.launch { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) } },
|
||||
dropdownMenuItems = {
|
||||
tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) {
|
||||
ContactMenuItems(chat, chat.chatInfo.contact, chatModel, showMenu, showMarkRead)
|
||||
@@ -75,6 +80,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
}
|
||||
is ChatInfo.Group ->
|
||||
@@ -84,7 +90,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, disabled, linkMode, inProgress.value, progressByTimeout)
|
||||
}
|
||||
},
|
||||
click = { if (!inProgress.value) groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel, inProgress) },
|
||||
click = { if (!inProgress.value) scope.launch { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel, inProgress) } },
|
||||
dropdownMenuItems = {
|
||||
tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) {
|
||||
GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, inProgress, showMarkRead)
|
||||
@@ -94,6 +100,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
is ChatInfo.Local -> {
|
||||
ChatListNavLinkLayout(
|
||||
@@ -102,7 +109,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, disabled, linkMode, inProgress = false, progressByTimeout = false)
|
||||
}
|
||||
},
|
||||
click = { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) },
|
||||
click = { scope.launch { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) } },
|
||||
dropdownMenuItems = {
|
||||
tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) {
|
||||
NoteFolderMenuItems(chat, showMenu, showMarkRead)
|
||||
@@ -112,6 +119,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
}
|
||||
is ChatInfo.ContactRequest ->
|
||||
@@ -131,6 +139,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
is ChatInfo.ContactConnection ->
|
||||
ChatListNavLinkLayout(
|
||||
@@ -151,6 +160,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
is ChatInfo.InvalidJSON ->
|
||||
ChatListNavLinkLayout(
|
||||
@@ -167,53 +177,54 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ErrorChatListItem() {
|
||||
fun ErrorChatListItem() {
|
||||
Box(Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp)) {
|
||||
Text(stringResource(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic)
|
||||
}
|
||||
}
|
||||
|
||||
fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) {
|
||||
suspend fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) {
|
||||
when {
|
||||
contact.activeConn == null && contact.profile.contactLink != null -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true)
|
||||
else -> withBGApi { openChat(rhId, ChatInfo.Direct(contact), chatModel) }
|
||||
else -> openChat(rhId, ChatInfo.Direct(contact), chatModel)
|
||||
}
|
||||
}
|
||||
|
||||
fun groupChatAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState<Boolean>? = null) {
|
||||
suspend fun groupChatAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState<Boolean>? = null) {
|
||||
when (groupInfo.membership.memberStatus) {
|
||||
GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(rhId, groupInfo, chatModel, inProgress)
|
||||
GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert(rhId)
|
||||
else -> withBGApi { openChat(rhId, ChatInfo.Group(groupInfo), chatModel) }
|
||||
else -> openChat(rhId, ChatInfo.Group(groupInfo), chatModel)
|
||||
}
|
||||
}
|
||||
|
||||
fun noteFolderChatAction(rhId: Long?, noteFolder: NoteFolder) {
|
||||
withBGApi { openChat(rhId, ChatInfo.Local(noteFolder), chatModel) }
|
||||
suspend fun noteFolderChatAction(rhId: Long?, noteFolder: NoteFolder) {
|
||||
openChat(rhId, ChatInfo.Local(noteFolder), chatModel)
|
||||
}
|
||||
|
||||
suspend fun openDirectChat(rhId: Long?, contactId: Long, chatModel: ChatModel) {
|
||||
suspend fun openDirectChat(rhId: Long?, contactId: Long, chatModel: ChatModel) = coroutineScope {
|
||||
val chat = chatModel.controller.apiGetChat(rhId, ChatType.Direct, contactId)
|
||||
if (chat != null) {
|
||||
if (chat != null && isActive) {
|
||||
openLoadedChat(chat, chatModel)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun openGroupChat(rhId: Long?, groupId: Long, chatModel: ChatModel) {
|
||||
suspend fun openGroupChat(rhId: Long?, groupId: Long, chatModel: ChatModel) = coroutineScope {
|
||||
val chat = chatModel.controller.apiGetChat(rhId, ChatType.Group, groupId)
|
||||
if (chat != null) {
|
||||
if (chat != null && isActive) {
|
||||
openLoadedChat(chat, chatModel)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) {
|
||||
suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) = coroutineScope {
|
||||
val chat = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId)
|
||||
if (chat != null) {
|
||||
if (chat != null && isActive) {
|
||||
openLoadedChat(chat, chatModel)
|
||||
}
|
||||
}
|
||||
@@ -359,7 +370,7 @@ fun ToggleFavoritesChatAction(chat: Chat, chatModel: ChatModel, favorite: Boolea
|
||||
if (favorite) stringResource(MR.strings.unfavorite_chat) else stringResource(MR.strings.favorite_chat),
|
||||
if (favorite) painterResource(MR.images.ic_star_off) else painterResource(MR.images.ic_star),
|
||||
onClick = {
|
||||
toggleChatFavorite(chat, !favorite, chatModel)
|
||||
toggleChatFavorite(chat.remoteHostId, chat.chatInfo, !favorite, chatModel)
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
@@ -371,7 +382,7 @@ fun ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, ntfsEnabled:
|
||||
if (ntfsEnabled) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat),
|
||||
if (ntfsEnabled) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications),
|
||||
onClick = {
|
||||
toggleNotifications(chat, !ntfsEnabled, chatModel)
|
||||
toggleNotifications(chat.remoteHostId, chat.chatInfo, !ntfsEnabled, chatModel)
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
@@ -469,13 +480,13 @@ fun LeaveGroupAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, sh
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState<Boolean>, onSuccess: ((chat: Chat) -> Unit)? = null) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.accept_contact_button),
|
||||
painterResource(MR.images.ic_check),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
onClick = {
|
||||
acceptContactRequest(rhId, incognito = false, chatInfo.apiId, chatInfo, true, chatModel)
|
||||
acceptContactRequest(rhId, incognito = false, chatInfo.apiId, chatInfo, true, chatModel, onSuccess)
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
@@ -484,7 +495,7 @@ fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chat
|
||||
painterResource(MR.images.ic_theater_comedy),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
onClick = {
|
||||
acceptContactRequest(rhId, incognito = true, chatInfo.apiId, chatInfo, true, chatModel)
|
||||
acceptContactRequest(rhId, incognito = true, chatInfo.apiId, chatInfo, true, chatModel, onSuccess)
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
@@ -554,7 +565,9 @@ fun markChatRead(c: Chat, chatModel: ChatModel) {
|
||||
withApi {
|
||||
if (chat.chatStats.unreadCount > 0) {
|
||||
val minUnreadItemId = chat.chatStats.minUnreadItemId
|
||||
chatModel.markChatItemsRead(chat)
|
||||
withChats {
|
||||
markChatItemsRead(chat.remoteHostId, chat.chatInfo)
|
||||
}
|
||||
chatModel.controller.apiChatRead(
|
||||
chat.remoteHostId,
|
||||
chat.chatInfo.chatType,
|
||||
@@ -571,7 +584,9 @@ fun markChatRead(c: Chat, chatModel: ChatModel) {
|
||||
false
|
||||
)
|
||||
if (success) {
|
||||
chatModel.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
|
||||
withChats {
|
||||
replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -589,12 +604,14 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) {
|
||||
true
|
||||
)
|
||||
if (success) {
|
||||
chatModel.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true)))
|
||||
withChats {
|
||||
replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
|
||||
fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel, onSucess: ((chat: Chat) -> Unit)? = null) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.accept_connection_request__question),
|
||||
text = AnnotatedString(generalGetString(MR.strings.if_you_choose_to_reject_the_sender_will_not_be_notified)),
|
||||
@@ -602,13 +619,13 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque
|
||||
Column {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
acceptContactRequest(rhId, incognito = false, contactRequest.apiId, contactRequest, true, chatModel)
|
||||
acceptContactRequest(rhId, incognito = false, contactRequest.apiId, contactRequest, true, chatModel, onSucess)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.accept_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
acceptContactRequest(rhId, incognito = true, contactRequest.apiId, contactRequest, true, chatModel)
|
||||
acceptContactRequest(rhId, incognito = true, contactRequest.apiId, contactRequest, true, chatModel, onSucess)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
@@ -624,13 +641,16 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque
|
||||
)
|
||||
}
|
||||
|
||||
fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel) {
|
||||
fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel, close: ((chat: Chat) -> Unit)? = null ) {
|
||||
withBGApi {
|
||||
val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, apiId)
|
||||
if (contact != null && isCurrentUser && contactRequest != null) {
|
||||
val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf())
|
||||
chatModel.replaceChat(rhId, contactRequest.id, chat)
|
||||
withChats {
|
||||
replaceChat(rhId, contactRequest.id, chat)
|
||||
}
|
||||
chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected())
|
||||
close?.invoke(chat)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -638,7 +658,9 @@ fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRe
|
||||
fun rejectContactRequest(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
|
||||
withBGApi {
|
||||
chatModel.controller.apiRejectContactRequest(rhId, contactRequest.apiId)
|
||||
chatModel.removeChat(rhId, contactRequest.id)
|
||||
withChats {
|
||||
removeChat(rhId, contactRequest.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -654,7 +676,9 @@ fun deleteContactConnectionAlert(rhId: Long?, connection: PendingContactConnecti
|
||||
withBGApi {
|
||||
AlertManager.shared.hideAlert()
|
||||
if (chatModel.controller.apiDeleteChat(rhId, ChatType.ContactConnection, connection.apiId)) {
|
||||
chatModel.removeChat(rhId, connection.id)
|
||||
withChats {
|
||||
removeChat(rhId, connection.id)
|
||||
}
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
@@ -673,7 +697,9 @@ fun pendingContactAlertDialog(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatMo
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiDeleteChat(rhId, chatInfo.chatType, chatInfo.apiId)
|
||||
if (r) {
|
||||
chatModel.removeChat(rhId, chatInfo.id)
|
||||
withChats {
|
||||
removeChat(rhId, chatInfo.id)
|
||||
}
|
||||
if (chatModel.chatId.value == chatInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
@@ -735,7 +761,9 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress(
|
||||
suspend fun connectContactViaAddress(chatModel: ChatModel, rhId: Long?, contactId: Long, incognito: Boolean): Boolean {
|
||||
val contact = chatModel.controller.apiConnectContactViaAddress(rhId, incognito, contactId)
|
||||
if (contact != null) {
|
||||
chatModel.updateContact(rhId, contact)
|
||||
withChats {
|
||||
updateContact(rhId, contact)
|
||||
}
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
title = generalGetString(MR.strings.connection_request_sent),
|
||||
text = generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted),
|
||||
@@ -776,7 +804,9 @@ fun deleteGroup(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiDeleteChat(rhId, ChatType.Group, groupInfo.apiId)
|
||||
if (r) {
|
||||
chatModel.removeChat(rhId, groupInfo.id)
|
||||
withChats {
|
||||
removeChat(rhId, groupInfo.id)
|
||||
}
|
||||
if (chatModel.chatId.value == groupInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
@@ -794,22 +824,22 @@ fun groupInvitationAcceptedAlert(rhId: Long?) {
|
||||
)
|
||||
}
|
||||
|
||||
fun toggleNotifications(chat: Chat, enableAllNtfs: Boolean, chatModel: ChatModel, currentState: MutableState<Boolean>? = null) {
|
||||
val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(enableNtfs = if (enableAllNtfs) MsgFilter.All else MsgFilter.None)
|
||||
updateChatSettings(chat, chatSettings, chatModel, currentState)
|
||||
fun toggleNotifications(remoteHostId: Long?, chatInfo: ChatInfo, enableAllNtfs: Boolean, chatModel: ChatModel, currentState: MutableState<Boolean>? = null) {
|
||||
val chatSettings = (chatInfo.chatSettings ?: ChatSettings.defaults).copy(enableNtfs = if (enableAllNtfs) MsgFilter.All else MsgFilter.None)
|
||||
updateChatSettings(remoteHostId, chatInfo, chatSettings, chatModel, currentState)
|
||||
}
|
||||
|
||||
fun toggleChatFavorite(chat: Chat, favorite: Boolean, chatModel: ChatModel) {
|
||||
val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(favorite = favorite)
|
||||
updateChatSettings(chat, chatSettings, chatModel)
|
||||
fun toggleChatFavorite(remoteHostId: Long?, chatInfo: ChatInfo, favorite: Boolean, chatModel: ChatModel) {
|
||||
val chatSettings = (chatInfo.chatSettings ?: ChatSettings.defaults).copy(favorite = favorite)
|
||||
updateChatSettings(remoteHostId, chatInfo, chatSettings, chatModel)
|
||||
}
|
||||
|
||||
fun updateChatSettings(chat: Chat, chatSettings: ChatSettings, chatModel: ChatModel, currentState: MutableState<Boolean>? = null) {
|
||||
val newChatInfo = when(chat.chatInfo) {
|
||||
is ChatInfo.Direct -> with (chat.chatInfo) {
|
||||
fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: ChatSettings, chatModel: ChatModel, currentState: MutableState<Boolean>? = null) {
|
||||
val newChatInfo = when(chatInfo) {
|
||||
is ChatInfo.Direct -> with (chatInfo) {
|
||||
ChatInfo.Direct(contact.copy(chatSettings = chatSettings))
|
||||
}
|
||||
is ChatInfo.Group -> with(chat.chatInfo) {
|
||||
is ChatInfo.Group -> with(chatInfo) {
|
||||
ChatInfo.Group(groupInfo.copy(chatSettings = chatSettings))
|
||||
}
|
||||
else -> null
|
||||
@@ -817,17 +847,19 @@ fun updateChatSettings(chat: Chat, chatSettings: ChatSettings, chatModel: ChatMo
|
||||
withBGApi {
|
||||
val res = when (newChatInfo) {
|
||||
is ChatInfo.Direct -> with(newChatInfo) {
|
||||
chatModel.controller.apiSetSettings(chat.remoteHostId, chatType, apiId, contact.chatSettings)
|
||||
chatModel.controller.apiSetSettings(remoteHostId, chatType, apiId, contact.chatSettings)
|
||||
}
|
||||
is ChatInfo.Group -> with(newChatInfo) {
|
||||
chatModel.controller.apiSetSettings(chat.remoteHostId, chatType, apiId, groupInfo.chatSettings)
|
||||
chatModel.controller.apiSetSettings(remoteHostId, chatType, apiId, groupInfo.chatSettings)
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
if (res && newChatInfo != null) {
|
||||
chatModel.updateChatInfo(chat.remoteHostId, newChatInfo)
|
||||
withChats {
|
||||
updateChatInfo(remoteHostId, newChatInfo)
|
||||
}
|
||||
if (chatSettings.enableNtfs != MsgFilter.All) {
|
||||
ntfManager.cancelNotificationsForChat(chat.id)
|
||||
ntfManager.cancelNotificationsForChat(chatInfo.id)
|
||||
}
|
||||
val current = currentState?.value
|
||||
if (current != null) {
|
||||
@@ -846,6 +878,7 @@ expect fun ChatListNavLinkLayout(
|
||||
disabled: Boolean,
|
||||
selectedChat: State<Boolean>,
|
||||
nextChatSelected: State<Boolean>,
|
||||
oneHandUI: State<Boolean>
|
||||
)
|
||||
|
||||
@Preview/*(
|
||||
@@ -888,7 +921,8 @@ fun PreviewChatListNavLinkDirect() {
|
||||
showMenu = remember { mutableStateOf(false) },
|
||||
disabled = false,
|
||||
selectedChat = remember { mutableStateOf(false) },
|
||||
nextChatSelected = remember { mutableStateOf(false) }
|
||||
nextChatSelected = remember { mutableStateOf(false) },
|
||||
oneHandUI = remember { mutableStateOf(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -933,7 +967,8 @@ fun PreviewChatListNavLinkGroup() {
|
||||
showMenu = remember { mutableStateOf(false) },
|
||||
disabled = false,
|
||||
selectedChat = remember { mutableStateOf(false) },
|
||||
nextChatSelected = remember { mutableStateOf(false) }
|
||||
nextChatSelected = remember { mutableStateOf(false) },
|
||||
oneHandUI = remember { mutableStateOf(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -955,7 +990,8 @@ fun PreviewChatListNavLinkContactRequest() {
|
||||
showMenu = remember { mutableStateOf(false) },
|
||||
disabled = false,
|
||||
selectedChat = remember { mutableStateOf(false) },
|
||||
nextChatSelected = remember { mutableStateOf(false) }
|
||||
nextChatSelected = remember { mutableStateOf(false) },
|
||||
oneHandUI = remember { mutableStateOf(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+172
-77
@@ -12,7 +12,7 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
@@ -33,7 +33,6 @@ import chat.simplex.common.views.onboarding.shouldShowWhatsNew
|
||||
import chat.simplex.common.views.usersettings.SettingsView
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.call.Call
|
||||
import chat.simplex.common.views.chat.group.ProgressIndicator
|
||||
import chat.simplex.common.views.chat.item.CIFileViewScope
|
||||
import chat.simplex.common.views.newchat.*
|
||||
import chat.simplex.res.MR
|
||||
@@ -42,28 +41,37 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.net.URI
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
private fun showNewChatSheet(oneHandUI: State<Boolean>, barTitle: String) {
|
||||
ModalManager.start.closeModals()
|
||||
ModalManager.end.closeModals()
|
||||
chatModel.newChatSheetVisible.value = true
|
||||
ModalManager.start.showModalCloseable(
|
||||
closeOnTop = !oneHandUI.value,
|
||||
closeBarTitle = if (oneHandUI.value) barTitle else null,
|
||||
endButtons = { Spacer(Modifier.minimumInteractiveComponentSize()) }
|
||||
) { close ->
|
||||
NewChatSheet(rh = chatModel.currentRemoteHost.value, close)
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
chatModel.newChatSheetVisible.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
|
||||
val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
|
||||
val showNewChatSheet = {
|
||||
newChatSheetState.value = AnimatedViewState.VISIBLE
|
||||
}
|
||||
val hideNewChatSheet: (animated: Boolean) -> Unit = { animated ->
|
||||
if (animated) newChatSheetState.value = AnimatedViewState.HIDING
|
||||
else newChatSheetState.value = AnimatedViewState.GONE
|
||||
}
|
||||
val oneHandUI = remember { chatModel.controller.appPrefs.oneHandUI }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (shouldShowWhatsNew(chatModel)) {
|
||||
delay(1000L)
|
||||
ModalManager.center.showCustomModal { close -> WhatsNewView(close = close) }
|
||||
}
|
||||
}
|
||||
LaunchedEffect(chatModel.clearOverlays.value) {
|
||||
if (chatModel.clearOverlays.value && newChatSheetState.value.isVisible()) hideNewChatSheet(false)
|
||||
}
|
||||
|
||||
if (appPlatform.isDesktop) {
|
||||
KeyChangeEffect(chatModel.chatId.value) {
|
||||
if (chatModel.chatId.value != null) {
|
||||
@@ -77,7 +85,31 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val (userPickerState, scaffoldState ) = settingsState
|
||||
Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(scaffoldState.drawerState, userPickerState, stopped)} },
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (!oneHandUI.state.value) {
|
||||
Box(Modifier.padding(end = endPadding)) {
|
||||
ChatListToolbar(
|
||||
scaffoldState.drawerState,
|
||||
userPickerState,
|
||||
stopped,
|
||||
oneHandUI
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
if (oneHandUI.state.value) {
|
||||
Box(Modifier.padding(end = endPadding)) {
|
||||
ChatListToolbar(
|
||||
scaffoldState.drawerState,
|
||||
userPickerState,
|
||||
stopped,
|
||||
oneHandUI
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
scaffoldState = scaffoldState,
|
||||
drawerContent = {
|
||||
tryOrShowError("Settings", error = { ErrorSettingsView() }) {
|
||||
@@ -89,11 +121,11 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f),
|
||||
drawerGesturesEnabled = appPlatform.isAndroid,
|
||||
floatingActionButton = {
|
||||
if (searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
|
||||
if (!oneHandUI.state.value && searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
if (!stopped) {
|
||||
if (newChatSheetState.value.isVisible()) hideNewChatSheet(true) else showNewChatSheet()
|
||||
showNewChatSheet(oneHandUI.state, generalGetString(MR.strings.new_chat))
|
||||
}
|
||||
},
|
||||
Modifier
|
||||
@@ -108,25 +140,33 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
contentColor = Color.White
|
||||
) {
|
||||
Icon(if (!newChatSheetState.collectAsState().value.isVisible()) painterResource(MR.images.ic_edit_filled) else painterResource(MR.images.ic_close), stringResource(MR.strings.add_contact_or_create_group), Modifier.size(24.dp * fontSizeSqrtMultiplier))
|
||||
Icon(painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group), Modifier.size(24.dp * fontSizeSqrtMultiplier))
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Box(Modifier.padding(it).padding(end = endPadding)) {
|
||||
var modifier = Modifier.padding(it).padding(end = endPadding)
|
||||
if (oneHandUI.state.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Box(modifier) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
if (!chatModel.desktopNoUserNoRemote) {
|
||||
ChatList(chatModel, searchText = searchText)
|
||||
ChatList(chatModel, searchText = searchText, oneHandUI = oneHandUI)
|
||||
}
|
||||
if (chatModel.chats.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) {
|
||||
Text(stringResource(
|
||||
if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary)
|
||||
if (!stopped && !newChatSheetState.collectAsState().value.isVisible() && chatModel.chatRunning.value == true && searchText.value.text.isEmpty()) {
|
||||
OnboardingButtons(showNewChatSheet)
|
||||
if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) {
|
||||
var textModifier = Modifier.align(Alignment.Center)
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
textModifier = textModifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Text(stringResource(
|
||||
if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats), textModifier, color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,17 +175,17 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
if (appPlatform.isDesktop) {
|
||||
val call = remember { chatModel.activeCall }.value
|
||||
if (call != null) {
|
||||
ActiveCallInteractiveArea(call, newChatSheetState)
|
||||
ActiveCallInteractiveArea(call)
|
||||
}
|
||||
}
|
||||
// TODO disable this button and sheet for the duration of the switch
|
||||
tryOrShowError("NewChatSheet", error = {}) {
|
||||
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
|
||||
}
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
tryOrShowError("UserPicker", error = {}) {
|
||||
UserPicker(chatModel, userPickerState) {
|
||||
UserPicker(
|
||||
chatModel = chatModel,
|
||||
userPickerState = userPickerState,
|
||||
contentAlignment = if (oneHandUI.state.value) Alignment.BottomStart else Alignment.TopStart
|
||||
) {
|
||||
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
@@ -153,27 +193,6 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnboardingButtons(openNewChatSheet: () -> Unit) {
|
||||
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING), horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Bottom) {
|
||||
ConnectButton(generalGetString(MR.strings.tap_to_start_new_chat), openNewChatSheet)
|
||||
val color = MaterialTheme.colors.primaryVariant
|
||||
Canvas(modifier = Modifier.width(40.dp).height(10.dp), onDraw = {
|
||||
val trianglePath = Path().apply {
|
||||
moveTo(0.dp.toPx(), 0f)
|
||||
lineTo(16.dp.toPx(), 0.dp.toPx())
|
||||
lineTo(8.dp.toPx(), 10.dp.toPx())
|
||||
lineTo(0.dp.toPx(), 0.dp.toPx())
|
||||
}
|
||||
drawPath(
|
||||
color = color,
|
||||
path = trianglePath
|
||||
)
|
||||
})
|
||||
Spacer(Modifier.height(62.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConnectButton(text: String, onClick: () -> Unit) {
|
||||
Button(
|
||||
@@ -191,10 +210,37 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean) {
|
||||
private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean, oneHandUI: SharedPreference<Boolean>) {
|
||||
val serversSummary: MutableState<PresentedServersSummary?> = remember { mutableStateOf(null) }
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
val updatingProgress = remember { chatModel.updatingProgress }.value
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
val sp16 = with(LocalDensity.current) { 16.sp.toDp() }
|
||||
|
||||
barButtons.add {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (!stopped) {
|
||||
showNewChatSheet(oneHandUI.state, generalGetString(MR.strings.new_chat))
|
||||
}
|
||||
},
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, shape = CircleShape)
|
||||
.padding(DEFAULT_PADDING_HALF)
|
||||
){
|
||||
Icon(
|
||||
painterResource(MR.images.ic_edit_filled),
|
||||
stringResource(MR.strings.add_contact_or_create_group),
|
||||
Modifier.size(sp16),
|
||||
tint = if (!stopped) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onSecondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updatingProgress != null) {
|
||||
barButtons.add {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
@@ -291,10 +337,12 @@ fun SubscriptionStatusIndicator(click: (() -> Unit)) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
suspend fun setSubsTotal() {
|
||||
val r = chatModel.controller.getAgentSubsTotal(chatModel.remoteHostId())
|
||||
if (r != null) {
|
||||
subs = r.first
|
||||
hasSess = r.second
|
||||
if (chatModel.currentUser.value != null) {
|
||||
val r = chatModel.controller.getAgentSubsTotal(chatModel.remoteHostId())
|
||||
if (r != null) {
|
||||
subs = r.first
|
||||
hasSess = r.second
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,6 +390,7 @@ fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> U
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.unreadBadge(text: String? = "") {
|
||||
Text(
|
||||
@@ -377,7 +426,7 @@ private fun ToggleFilterEnabledButton() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
expect fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow<AnimatedViewState>)
|
||||
expect fun ActiveCallInteractiveArea(call: Call)
|
||||
|
||||
fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
|
||||
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
|
||||
@@ -391,11 +440,22 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, searchChatFilteredBySimplexLink: MutableState<String?>) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, searchChatFilteredBySimplexLink: MutableState<String?>, oneHandUI: SharedPreference<Boolean>) {
|
||||
var modifier = Modifier.fillMaxWidth();
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
Icon(painterResource(MR.images.ic_search), null, Modifier.padding(horizontal = DEFAULT_PADDING_HALF).size(24.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.secondary)
|
||||
Icon(
|
||||
painterResource(MR.images.ic_search),
|
||||
contentDescription = null,
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(24.dp * fontSizeSqrtMultiplier),
|
||||
tint = MaterialTheme.colors.secondary
|
||||
)
|
||||
SearchTextField(
|
||||
Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester),
|
||||
placeholder = stringResource(MR.strings.search_or_paste_simplex_link),
|
||||
@@ -415,7 +475,7 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState
|
||||
}
|
||||
} else {
|
||||
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
|
||||
if (chatModel.chats.size > 0) {
|
||||
if (chatModel.chats.value.isNotEmpty()) {
|
||||
ToggleFilterEnabledButton()
|
||||
}
|
||||
Spacer(Modifier.width(padding))
|
||||
@@ -481,9 +541,35 @@ private fun ErrorSettingsView() {
|
||||
|
||||
private var lazyListState = 0 to 0
|
||||
|
||||
enum class ScrollDirection {
|
||||
Up, Down, Idle
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldValue>) {
|
||||
private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldValue>, oneHandUI: SharedPreference<Boolean>) {
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) }
|
||||
var previousIndex by remember { mutableStateOf(0) }
|
||||
var previousScrollOffset by remember { mutableStateOf(0) }
|
||||
|
||||
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
||||
val currentIndex = listState.firstVisibleItemIndex
|
||||
val currentScrollOffset = listState.firstVisibleItemScrollOffset
|
||||
val threshold = 25
|
||||
|
||||
scrollDirection = when {
|
||||
currentIndex > previousIndex -> ScrollDirection.Down
|
||||
currentIndex < previousIndex -> ScrollDirection.Up
|
||||
currentScrollOffset > previousScrollOffset + threshold -> ScrollDirection.Down
|
||||
currentScrollOffset < previousScrollOffset - threshold -> ScrollDirection.Up
|
||||
currentScrollOffset == previousScrollOffset -> ScrollDirection.Idle
|
||||
else -> scrollDirection
|
||||
}
|
||||
|
||||
previousIndex = currentIndex
|
||||
previousScrollOffset = currentScrollOffset
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||
}
|
||||
@@ -494,7 +580,7 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
||||
// val chats by remember(search, showUnreadAndFavorites) { derivedStateOf { filteredChats(showUnreadAndFavorites, search, allChats.toList()) } }
|
||||
val searchShowingSimplexLink = remember { mutableStateOf(false) }
|
||||
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
|
||||
val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.toList())
|
||||
val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList())
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
listState
|
||||
@@ -504,7 +590,9 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
||||
Modifier
|
||||
.offset {
|
||||
val y = if (searchText.value.text.isEmpty()) {
|
||||
if (listState.firstVisibleItemIndex == 0) -listState.firstVisibleItemScrollOffset else -1000
|
||||
if (oneHandUI.state.value && scrollDirection == ScrollDirection.Up) {
|
||||
0
|
||||
} else if (listState.firstVisibleItemIndex == 0) -listState.firstVisibleItemScrollOffset else -1000
|
||||
} else {
|
||||
0
|
||||
}
|
||||
@@ -512,7 +600,7 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
||||
}
|
||||
.background(MaterialTheme.colors.background)
|
||||
) {
|
||||
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
|
||||
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink, oneHandUI)
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
@@ -520,11 +608,17 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
||||
val nextChatSelected = remember(chat.id, chats) { derivedStateOf {
|
||||
chatModel.chatId.value != null && chats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
} }
|
||||
ChatListNavLinkView(chat, nextChatSelected)
|
||||
ChatListNavLinkView(chat, nextChatSelected, oneHandUI.state)
|
||||
}
|
||||
}
|
||||
if (chats.isEmpty() && chatModel.chats.isNotEmpty()) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
if (chats.isEmpty() && chatModel.chats.value.isNotEmpty()) {
|
||||
var modifier = Modifier.fillMaxSize();
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Box(modifier, contentAlignment = Alignment.Center) {
|
||||
Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
@@ -543,17 +637,18 @@ private fun filteredChats(
|
||||
} else {
|
||||
val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase()
|
||||
if (s.isEmpty() && !showUnreadAndFavorites)
|
||||
chats
|
||||
chats.filter { chat -> !chat.chatInfo.chatDeleted && chatContactType(chat) != ContactType.CARD }
|
||||
else {
|
||||
chats.filter { chat ->
|
||||
when (val cInfo = chat.chatInfo) {
|
||||
is ChatInfo.Direct -> if (s.isEmpty()) {
|
||||
chat.id == chatModel.chatId.value || filtered(chat)
|
||||
} else {
|
||||
(viewNameContains(cInfo, s) ||
|
||||
cInfo.contact.profile.displayName.lowercase().contains(s) ||
|
||||
cInfo.contact.fullName.lowercase().contains(s))
|
||||
}
|
||||
is ChatInfo.Direct -> chatContactType(chat) != ContactType.CARD && !chat.chatInfo.chatDeleted && (
|
||||
if (s.isEmpty()) {
|
||||
chat.id == chatModel.chatId.value || filtered(chat)
|
||||
} else {
|
||||
(viewNameContains(cInfo, s) ||
|
||||
cInfo.contact.profile.displayName.lowercase().contains(s) ||
|
||||
cInfo.contact.fullName.lowercase().contains(s))
|
||||
})
|
||||
is ChatInfo.Group -> if (s.isEmpty()) {
|
||||
chat.id == chatModel.chatId.value || filtered(chat) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -207,7 +207,7 @@ fun ChatPreviewView(
|
||||
} else {
|
||||
when (cInfo) {
|
||||
is ChatInfo.Direct ->
|
||||
if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null) {
|
||||
if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null && cInfo.contact.active) {
|
||||
Text(stringResource(MR.strings.contact_tap_to_connect), color = MaterialTheme.colors.primary)
|
||||
} else if (!cInfo.contact.sndReady && cInfo.contact.activeConn != null) {
|
||||
if (cInfo.contact.nextSendGrpInv) {
|
||||
|
||||
+3
-1
@@ -719,7 +719,9 @@ fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableSta
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
suspend fun setServersSummary() {
|
||||
serversSummary.value = chatModel.controller.getAgentServersSummary(chatModel.remoteHostId())
|
||||
if (chatModel.currentUser.value != null) {
|
||||
serversSummary.value = chatModel.controller.getAgentServersSummary(chatModel.remoteHostId())
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
|
||||
+20
-10
@@ -6,6 +6,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -13,6 +14,7 @@ import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ShareListNavLinkView(
|
||||
@@ -20,19 +22,21 @@ fun ShareListNavLinkView(
|
||||
chatModel: ChatModel,
|
||||
isMediaOrFileAttachment: Boolean,
|
||||
isVoice: Boolean,
|
||||
hasSimplexLink: Boolean
|
||||
hasSimplexLink: Boolean,
|
||||
oneHandUI: State<Boolean>
|
||||
) {
|
||||
val stopped = chatModel.chatRunning.value == false
|
||||
val scope = rememberCoroutineScope()
|
||||
when (chat.chatInfo) {
|
||||
is ChatInfo.Direct -> {
|
||||
val voiceProhibited = isVoice && !chat.chatInfo.featureEnabled(ChatFeature.Voice)
|
||||
ShareListNavLinkLayout(
|
||||
chatLinkPreview = { SharePreviewView(chat, disabled = voiceProhibited) },
|
||||
chatLinkPreview = { SharePreviewView(chat, disabled = voiceProhibited, oneHandUI = oneHandUI) },
|
||||
click = {
|
||||
if (voiceProhibited) {
|
||||
showForwardProhibitedByPrefAlert()
|
||||
} else {
|
||||
directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel)
|
||||
scope.launch { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) }
|
||||
}
|
||||
},
|
||||
stopped
|
||||
@@ -44,12 +48,12 @@ fun ShareListNavLinkView(
|
||||
val voiceProhibited = isVoice && !chat.chatInfo.featureEnabled(ChatFeature.Voice)
|
||||
val prohibitedByPref = simplexLinkProhibited || fileProhibited || voiceProhibited
|
||||
ShareListNavLinkLayout(
|
||||
chatLinkPreview = { SharePreviewView(chat, disabled = prohibitedByPref) },
|
||||
chatLinkPreview = { SharePreviewView(chat, disabled = prohibitedByPref, oneHandUI = oneHandUI) },
|
||||
click = {
|
||||
if (prohibitedByPref) {
|
||||
showForwardProhibitedByPrefAlert()
|
||||
} else {
|
||||
groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel)
|
||||
scope.launch { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel) }
|
||||
}
|
||||
},
|
||||
stopped
|
||||
@@ -57,8 +61,8 @@ fun ShareListNavLinkView(
|
||||
}
|
||||
is ChatInfo.Local ->
|
||||
ShareListNavLinkLayout(
|
||||
chatLinkPreview = { SharePreviewView(chat, disabled = false) },
|
||||
click = { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) },
|
||||
chatLinkPreview = { SharePreviewView(chat, disabled = false, oneHandUI = oneHandUI) },
|
||||
click = { scope.launch { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) } },
|
||||
stopped
|
||||
)
|
||||
is ChatInfo.ContactRequest, is ChatInfo.ContactConnection, is ChatInfo.InvalidJSON -> {}
|
||||
@@ -76,7 +80,7 @@ private fun showForwardProhibitedByPrefAlert() {
|
||||
private fun ShareListNavLinkLayout(
|
||||
chatLinkPreview: @Composable () -> Unit,
|
||||
click: () -> Unit,
|
||||
stopped: Boolean
|
||||
stopped: Boolean,
|
||||
) {
|
||||
SectionItemView(minHeight = 50.dp, click = click, disabled = stopped) {
|
||||
chatLinkPreview()
|
||||
@@ -85,9 +89,15 @@ private fun ShareListNavLinkLayout(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SharePreviewView(chat: Chat, disabled: Boolean) {
|
||||
private fun SharePreviewView(chat: Chat, disabled: Boolean, oneHandUI: State<Boolean>) {
|
||||
var modifier = Modifier.fillMaxSize()
|
||||
|
||||
if (oneHandUI.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier.fillMaxSize(),
|
||||
modifier,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
+31
-13
@@ -7,6 +7,7 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
@@ -24,13 +25,16 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
|
||||
var searchInList by rememberSaveable { mutableStateOf("") }
|
||||
val (userPickerState, scaffoldState) = settingsState
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
val oneHandUI = remember { chatModel.controller.appPrefs.oneHandUI }
|
||||
|
||||
Scaffold(
|
||||
Modifier.padding(end = endPadding),
|
||||
contentColor = LocalContentColor.current,
|
||||
drawerContentColor = LocalContentColor.current,
|
||||
scaffoldState = scaffoldState,
|
||||
topBar = { Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } },
|
||||
) {
|
||||
topBar = { if (!oneHandUI.state.value) Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } },
|
||||
bottomBar = { if (oneHandUI.state.value) Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } },
|
||||
) {
|
||||
val sharedContent = chatModel.sharedContent.value
|
||||
var isMediaOrFileAttachment = false
|
||||
var isVoice = false
|
||||
@@ -56,21 +60,27 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
var modifier = Modifier.fillMaxSize()
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Box(Modifier.padding(it)) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
modifier = modifier
|
||||
) {
|
||||
if (chatModel.chats.isNotEmpty()) {
|
||||
if (chatModel.chats.value.isNotEmpty()) {
|
||||
ShareList(
|
||||
chatModel,
|
||||
search = searchInList,
|
||||
isMediaOrFileAttachment = isMediaOrFileAttachment,
|
||||
isVoice = isVoice,
|
||||
hasSimplexLink = hasSimplexLink
|
||||
hasSimplexLink = hasSimplexLink,
|
||||
oneHandUI = oneHandUI.state
|
||||
)
|
||||
} else {
|
||||
EmptyList()
|
||||
EmptyList(oneHandUI = oneHandUI.state)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,8 +101,14 @@ private fun hasSimplexLink(msg: String): Boolean {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyList() {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
private fun EmptyList(oneHandUI: State<Boolean>) {
|
||||
var modifier = Modifier.fillMaxSize()
|
||||
|
||||
if (oneHandUI.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Box(modifier, contentAlignment = Alignment.Center) {
|
||||
Text(stringResource(MR.strings.you_have_no_chats), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
@@ -127,7 +143,7 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
|
||||
})
|
||||
}
|
||||
}
|
||||
if (chatModel.chats.size >= 8) {
|
||||
if (chatModel.chats.value.size >= 8) {
|
||||
barButtons.add {
|
||||
IconButton({ showSearch = true }) {
|
||||
Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary)
|
||||
@@ -182,11 +198,12 @@ private fun ShareList(
|
||||
search: String,
|
||||
isMediaOrFileAttachment: Boolean,
|
||||
isVoice: Boolean,
|
||||
hasSimplexLink: Boolean
|
||||
hasSimplexLink: Boolean,
|
||||
oneHandUI: State<Boolean>
|
||||
) {
|
||||
val chats by remember(search) {
|
||||
derivedStateOf {
|
||||
val sorted = chatModel.chats.toList().sortedByDescending { it.chatInfo is ChatInfo.Local }
|
||||
val sorted = chatModel.chats.value.toList().sortedByDescending { it.chatInfo is ChatInfo.Local }
|
||||
if (search.isEmpty()) {
|
||||
sorted.filter { it.chatInfo.ready }
|
||||
} else {
|
||||
@@ -203,7 +220,8 @@ private fun ShareList(
|
||||
chatModel,
|
||||
isMediaOrFileAttachment = isMediaOrFileAttachment,
|
||||
isVoice = isVoice,
|
||||
hasSimplexLink = hasSimplexLink
|
||||
hasSimplexLink = hasSimplexLink,
|
||||
oneHandUI = oneHandUI
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -40,6 +40,7 @@ fun UserPicker(
|
||||
chatModel: ChatModel,
|
||||
userPickerState: MutableStateFlow<AnimatedViewState>,
|
||||
showSettings: Boolean = true,
|
||||
contentAlignment: Alignment = Alignment.TopStart,
|
||||
showCancel: Boolean = false,
|
||||
cancelClicked: () -> Unit = {},
|
||||
useFromDesktopClicked: () -> Unit = {},
|
||||
@@ -149,7 +150,8 @@ fun UserPicker(
|
||||
.graphicsLayer {
|
||||
alpha = animatedFloat.value
|
||||
translationY = (animatedFloat.value - 1) * xOffset
|
||||
}
|
||||
},
|
||||
contentAlignment = contentAlignment
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package chat.simplex.common.views.contacts
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.ContactType
|
||||
import chat.simplex.common.views.newchat.chatContactType
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
private fun onRequestAccepted(chat: Chat) {
|
||||
val chatInfo = chat.chatInfo
|
||||
if (chatInfo is ChatInfo.Direct) {
|
||||
ModalManager.start.closeModals()
|
||||
if (chatInfo.contact.sndReady) {
|
||||
openLoadedChat(chat, chatModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, oneHandUI: State<Boolean>) {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val rhId = chat.remoteHostId
|
||||
val disabled = chatModel.chatRunning.value == false || chatModel.deletedChats.value.contains(rhId to chat.chatInfo.id)
|
||||
val contactType = chatContactType(chat)
|
||||
|
||||
LaunchedEffect(chat.id) {
|
||||
showMenu.value = false
|
||||
delay(500L)
|
||||
}
|
||||
|
||||
val selectedChat = remember(chat.id) { derivedStateOf { chat.id == chatModel.chatId.value } }
|
||||
val view = LocalMultiplatformView()
|
||||
|
||||
when (chat.chatInfo) {
|
||||
is ChatInfo.Direct -> {
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = {
|
||||
tryOrShowError("${chat.id}ContactListNavLink", error = { ErrorChatListItem() }) {
|
||||
ContactPreviewView(chat, disabled)
|
||||
}
|
||||
},
|
||||
click = {
|
||||
hideKeyboard(view)
|
||||
when (contactType) {
|
||||
ContactType.RECENT -> {
|
||||
withApi {
|
||||
openChat(rhId, chat.chatInfo, chatModel)
|
||||
ModalManager.start.closeModals()
|
||||
}
|
||||
}
|
||||
ContactType.CHAT_DELETED -> {
|
||||
withApi {
|
||||
openChat(rhId, chat.chatInfo, chatModel)
|
||||
withChats {
|
||||
updateContact(rhId, chat.chatInfo.contact.copy(chatDeleted = false))
|
||||
}
|
||||
ModalManager.start.closeModals()
|
||||
}
|
||||
}
|
||||
ContactType.CARD -> {
|
||||
askCurrentOrIncognitoProfileConnectContactViaAddress(
|
||||
chatModel,
|
||||
rhId,
|
||||
chat.chatInfo.contact,
|
||||
close = { ModalManager.start.closeModals() },
|
||||
openChat = true
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
},
|
||||
dropdownMenuItems = {
|
||||
tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) {
|
||||
DeleteContactAction(chat, chatModel, showMenu)
|
||||
}
|
||||
},
|
||||
showMenu,
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
}
|
||||
is ChatInfo.ContactRequest -> {
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = {
|
||||
tryOrShowError("${chat.id}ContactListNavLink", error = { ErrorChatListItem() }) {
|
||||
ContactPreviewView(chat, disabled)
|
||||
}
|
||||
},
|
||||
click = {
|
||||
hideKeyboard(view)
|
||||
contactRequestAlertDialog(
|
||||
rhId,
|
||||
chat.chatInfo,
|
||||
chatModel,
|
||||
onSucess = { onRequestAccepted(it) }
|
||||
)
|
||||
},
|
||||
dropdownMenuItems = {
|
||||
tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) {
|
||||
ContactRequestMenuItems(
|
||||
rhId = chat.remoteHostId,
|
||||
chatInfo = chat.chatInfo,
|
||||
chatModel = chatModel,
|
||||
showMenu = showMenu,
|
||||
onSuccess = { onRequestAccepted(it) }
|
||||
)
|
||||
}
|
||||
},
|
||||
showMenu,
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.delete_contact_menu_action),
|
||||
painterResource(MR.images.ic_delete),
|
||||
onClick = {
|
||||
deleteContactDialog(chat, chatModel)
|
||||
showMenu.value = false
|
||||
},
|
||||
color = Color.Red
|
||||
)
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
package chat.simplex.common.views.contacts
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.newchat.ContactType
|
||||
import chat.simplex.common.views.newchat.chatContactType
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun ContactPreviewView(
|
||||
chat: Chat,
|
||||
disabled: Boolean,
|
||||
) {
|
||||
val cInfo = chat.chatInfo
|
||||
val contactType = chatContactType(chat)
|
||||
|
||||
@Composable
|
||||
fun VerifiedIcon() {
|
||||
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun chatPreviewTitle() {
|
||||
val deleting by remember(disabled, chat.id) { mutableStateOf(chatModel.deletedChats.value.contains(chat.remoteHostId to chat.chatInfo.id)) }
|
||||
|
||||
val textColor = when {
|
||||
deleting -> MaterialTheme.colors.secondary
|
||||
contactType == ContactType.CARD -> MaterialTheme.colors.primary
|
||||
contactType == ContactType.REQUEST -> MaterialTheme.colors.primary
|
||||
contactType == ContactType.RECENT && chat.chatInfo.incognito -> Indigo
|
||||
else -> Color.Unspecified
|
||||
}
|
||||
|
||||
when (cInfo) {
|
||||
is ChatInfo.Direct ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (cInfo.contact.verified) {
|
||||
VerifiedIcon()
|
||||
}
|
||||
Text(
|
||||
cInfo.chatViewName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = textColor
|
||||
)
|
||||
}
|
||||
is ChatInfo.ContactRequest ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
cInfo.chatViewName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = textColor
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(PaddingValues(horizontal = DEFAULT_PADDING_HALF)),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(contentAlignment = Alignment.BottomEnd) {
|
||||
ChatInfoImage(cInfo, size = 42.dp)
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON))
|
||||
|
||||
Box(modifier = Modifier.weight(10f, fill = true)) {
|
||||
chatPreviewTitle()
|
||||
}
|
||||
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
|
||||
if (chat.chatInfo is ChatInfo.ContactRequest) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_check),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.size(23.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (contactType == ContactType.CARD) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_mail),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.size(21.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (chat.chatInfo.chatSettings?.favorite == true) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_star_filled),
|
||||
contentDescription = generalGetString(MR.strings.favorite_chat),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.size(17.dp)
|
||||
)
|
||||
if (chat.chatInfo.incognito) {
|
||||
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (chat.chatInfo.incognito) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_theater_comedy),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.size(21.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-4
@@ -20,7 +20,7 @@ import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.updatingChatsMutex
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
@@ -502,7 +502,11 @@ fun deleteChatDatabaseFilesAndState() {
|
||||
// Clear sensitive data on screen just in case ModalManager will fail to prevent hiding its modals while database encrypts itself
|
||||
chatModel.chatId.value = null
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chats.clear()
|
||||
withLongRunningApi {
|
||||
withChats {
|
||||
chats.clear()
|
||||
}
|
||||
}
|
||||
chatModel.users.clear()
|
||||
ntfManager.cancelAllNotifications()
|
||||
}
|
||||
@@ -714,10 +718,10 @@ private fun afterSetCiTTL(
|
||||
appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath)
|
||||
withApi {
|
||||
try {
|
||||
updatingChatsMutex.withLock {
|
||||
withChats {
|
||||
// this is using current remote host on purpose - if it changes during update, it will load correct chats
|
||||
val chats = m.controller.apiGetChats(m.remoteHostId())
|
||||
m.updateChats(chats)
|
||||
updateChats(chats)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "apiGetChats error: ${e.message}")
|
||||
|
||||
+27
-4
@@ -11,6 +11,8 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.ui.theme.*
|
||||
@@ -18,17 +20,26 @@ import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
|
||||
@Composable
|
||||
fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Color = if (close != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, endButtons: @Composable RowScope.() -> Unit = {}) {
|
||||
fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Color = if (close != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, arrangement: Arrangement.Vertical = Arrangement.Top, closeBarTitle: String? = null, endButtons: @Composable RowScope.() -> Unit = {}) {
|
||||
var rowModifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(AppBarHeight * fontSizeSqrtMultiplier)
|
||||
|
||||
if (!closeBarTitle.isNullOrEmpty()) {
|
||||
rowModifier = rowModifier.background(MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f))
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
verticalArrangement = arrangement,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = AppBarHeight * fontSizeSqrtMultiplier)
|
||||
.padding(horizontal = AppBarHorizontalPadding)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = AppBarHorizontalPadding),
|
||||
content = {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().height(AppBarHeight * fontSizeSqrtMultiplier),
|
||||
rowModifier,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -37,6 +48,18 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Co
|
||||
} else {
|
||||
Spacer(Modifier)
|
||||
}
|
||||
if (!closeBarTitle.isNullOrEmpty()) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
closeBarTitle,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
}
|
||||
Row {
|
||||
endButtons()
|
||||
}
|
||||
|
||||
+2
-1
@@ -16,7 +16,7 @@ import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun DefaultTopAppBar(
|
||||
navigationButton: @Composable RowScope.() -> Unit,
|
||||
navigationButton: (@Composable RowScope.() -> Unit)? = null,
|
||||
title: (@Composable () -> Unit)?,
|
||||
onTitleClick: (() -> Unit)? = null,
|
||||
showSearch: Boolean,
|
||||
@@ -126,5 +126,6 @@ private fun TopAppBar(
|
||||
|
||||
val AppBarHeight = 56.dp
|
||||
val AppBarHorizontalPadding = 4.dp
|
||||
val BottomAppBarHeight = 60.dp
|
||||
private val TitleInsetWithoutIcon = DEFAULT_PADDING - AppBarHorizontalPadding
|
||||
val TitleInsetWithIcon = 72.dp
|
||||
|
||||
+24
-10
@@ -6,6 +6,7 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
@@ -24,6 +25,8 @@ fun ModalView(
|
||||
enableClose: Boolean = true,
|
||||
background: Color = MaterialTheme.colors.background,
|
||||
modifier: Modifier = Modifier,
|
||||
closeOnTop: Boolean = true,
|
||||
closeBarTitle: String? = null,
|
||||
endButtons: @Composable RowScope.() -> Unit = {},
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
@@ -32,8 +35,16 @@ fun ModalView(
|
||||
}
|
||||
Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) {
|
||||
Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) {
|
||||
CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons)
|
||||
Box(modifier) { content() }
|
||||
if (closeOnTop) {
|
||||
CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons)
|
||||
}
|
||||
Box(if (closeOnTop) modifier else modifier.padding(bottom = AppBarHeight * fontSizeSqrtMultiplier)) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
if (!closeOnTop) {
|
||||
CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons, arrangement = Arrangement.Bottom, closeBarTitle = closeBarTitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,24 +64,25 @@ class ModalData {
|
||||
|
||||
class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
private val modalViews = arrayListOf<Triple<Boolean, ModalData, (@Composable ModalData.(close: () -> Unit) -> Unit)>>()
|
||||
private val modalCount = mutableStateOf(0)
|
||||
private val _modalCount = mutableStateOf(0)
|
||||
val modalCount: State<Int> = _modalCount
|
||||
private val toRemove = mutableSetOf<Int>()
|
||||
private var oldViewChanging = AtomicBoolean(false)
|
||||
// Don't use mutableStateOf() here, because it produces this if showing from SimpleXAPI.startChat():
|
||||
// java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied
|
||||
private var passcodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
|
||||
|
||||
fun showModal(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
|
||||
fun showModal(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, closeBarTitle: String? = null,endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
|
||||
val data = ModalData()
|
||||
showCustomModal { close ->
|
||||
ModalView(close, showClose = showClose, endButtons = endButtons, content = { data.content() })
|
||||
ModalView(close, showClose = showClose, closeOnTop = closeOnTop, closeBarTitle = closeBarTitle, endButtons = endButtons, content = { data.content() })
|
||||
}
|
||||
}
|
||||
|
||||
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, closeBarTitle: String? = null, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||
val data = ModalData()
|
||||
showCustomModal { close ->
|
||||
ModalView(close, showClose = showClose, endButtons = endButtons, content = { data.content(close) })
|
||||
ModalView(close, showClose = showClose, endButtons = endButtons, closeOnTop = closeOnTop, closeBarTitle = closeBarTitle, content = { data.content(close) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +98,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
// to prevent unneeded animation on different situations
|
||||
val anim = if (appPlatform.isAndroid) animated else animated && (modalCount.value > 0 || placement == ModalPlacement.START)
|
||||
modalViews.add(Triple(anim, data, modal))
|
||||
modalCount.value = modalViews.size - toRemove.size
|
||||
_modalCount.value = modalViews.size - toRemove.size
|
||||
|
||||
if (placement == ModalPlacement.CENTER) {
|
||||
ChatModel.chatId.value = null
|
||||
@@ -105,18 +117,20 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
val hasModalsOpen: Boolean
|
||||
@Composable get () = remember { modalCount }.value > 0
|
||||
|
||||
fun openModalCount() = modalCount.value
|
||||
|
||||
fun closeModal() {
|
||||
if (modalViews.isNotEmpty()) {
|
||||
if (modalViews.lastOrNull()?.first == false) modalViews.removeAt(modalViews.lastIndex)
|
||||
else runAtomically { toRemove.add(modalViews.lastIndex - min(toRemove.size, modalViews.lastIndex)) }
|
||||
}
|
||||
modalCount.value = modalViews.size - toRemove.size
|
||||
_modalCount.value = modalViews.size - toRemove.size
|
||||
}
|
||||
|
||||
fun closeModals() {
|
||||
modalViews.clear()
|
||||
toRemove.clear()
|
||||
modalCount.value = 0
|
||||
_modalCount.value = 0
|
||||
}
|
||||
|
||||
fun closeModalsExceptFirst() {
|
||||
|
||||
+10
-6
@@ -18,6 +18,7 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.group.AddGroupMembersView
|
||||
import chat.simplex.common.views.chatlist.setGroupMembers
|
||||
@@ -32,19 +33,22 @@ import kotlinx.coroutines.launch
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) {
|
||||
fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, closeAll: () -> Unit) {
|
||||
val rhId = rh?.remoteHostId
|
||||
AddGroupLayout(
|
||||
createGroup = { incognito, groupProfile ->
|
||||
withBGApi {
|
||||
val groupInfo = chatModel.controller.apiNewGroup(rhId, incognito, groupProfile)
|
||||
if (groupInfo != null) {
|
||||
chatModel.updateGroup(rhId = rhId, groupInfo)
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chatItemStatuses.clear()
|
||||
chatModel.chatId.value = groupInfo.id
|
||||
withChats {
|
||||
updateGroup(rhId = rhId, groupInfo)
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chatItemStatuses.clear()
|
||||
chatModel.chatId.value = groupInfo.id
|
||||
}
|
||||
setGroupMembers(rhId, groupInfo, chatModel)
|
||||
close.invoke()
|
||||
closeAll.invoke()
|
||||
|
||||
if (!groupInfo.incognito) {
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
AddGroupMembersView(rhId, groupInfo, creatingGroup = true, chatModel, close)
|
||||
|
||||
+4
-1
@@ -7,6 +7,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -331,7 +332,9 @@ suspend fun connectViaUri(
|
||||
val pcc = chatModel.controller.apiConnect(rhId, incognito, uri.toString())
|
||||
val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION
|
||||
if (pcc != null) {
|
||||
chatModel.updateContactConnection(rhId, pcc)
|
||||
withChats {
|
||||
updateContactConnection(rhId, pcc)
|
||||
}
|
||||
close?.invoke()
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
title = generalGetString(MR.strings.connection_request_sent),
|
||||
|
||||
+4
-1
@@ -22,6 +22,7 @@ import chat.simplex.common.views.chat.LocalAliasEditor
|
||||
import chat.simplex.common.views.chatlist.deleteContactConnectionAlert
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.model.PendingContactConnection
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
@@ -186,7 +187,9 @@ fun DeleteButton(onClick: () -> Unit) {
|
||||
|
||||
private fun setContactAlias(rhId: Long?, contactConnection: PendingContactConnection, localAlias: String, chatModel: ChatModel) = withBGApi {
|
||||
chatModel.controller.apiSetConnectionAlias(rhId, contactConnection.pccConnId, localAlias)?.let {
|
||||
chatModel.updateContactConnection(rhId, it)
|
||||
withChats {
|
||||
updateContactConnection(rhId, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+550
-137
@@ -1,177 +1,597 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionView
|
||||
import TextIconSpaced
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chatlist.ScrollDirection
|
||||
import chat.simplex.common.views.contacts.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedViewState>, stopped: Boolean, closeNewChatSheet: (animated: Boolean) -> Unit) {
|
||||
// TODO close new chat if remote host changes in model
|
||||
if (newChatSheetState.collectAsState().value.isVisible()) BackHandler { closeNewChatSheet(true) }
|
||||
NewChatSheetLayout(
|
||||
newChatSheetState,
|
||||
stopped,
|
||||
addContact = {
|
||||
closeNewChatSheet(false)
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = close) }
|
||||
},
|
||||
scanPaste = {
|
||||
closeNewChatSheet(false)
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = true, close = close) }
|
||||
},
|
||||
createGroup = {
|
||||
closeNewChatSheet(false)
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close) }
|
||||
},
|
||||
closeNewChatSheet,
|
||||
)
|
||||
fun NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) {
|
||||
val oneHandUI = remember { chatModel.controller.appPrefs.oneHandUI }
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
if (!oneHandUI.state.value) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
stringResource(MR.strings.new_chat),
|
||||
hostDevice(rh?.remoteHostId),
|
||||
bottomPadding = bottomPadding
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val closeAll = { ModalManager.start.closeModals() }
|
||||
|
||||
var modifier = Modifier.fillMaxSize()
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Column(modifier = modifier) {
|
||||
NewChatSheetLayout(
|
||||
addContact = {
|
||||
ModalManager.start.showModalCloseable { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll ) }
|
||||
},
|
||||
scanPaste = {
|
||||
ModalManager.start.showModalCloseable { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = closeAll) }
|
||||
},
|
||||
createGroup = {
|
||||
ModalManager.start.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) }
|
||||
},
|
||||
rh = rh,
|
||||
close = close,
|
||||
oneHandUI = oneHandUI
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val titles = listOf(
|
||||
MR.strings.add_contact_tab,
|
||||
MR.strings.scan_paste_link,
|
||||
MR.strings.create_group_button
|
||||
)
|
||||
private val icons = listOf(MR.images.ic_add_link, MR.images.ic_qr_code, MR.images.ic_group)
|
||||
enum class ContactType {
|
||||
CARD, REQUEST, RECENT, CHAT_DELETED, UNLISTED
|
||||
}
|
||||
|
||||
fun chatContactType(chat: Chat): ContactType {
|
||||
return when (val cInfo = chat.chatInfo) {
|
||||
is ChatInfo.ContactRequest -> ContactType.REQUEST
|
||||
is ChatInfo.Direct -> {
|
||||
val contact = cInfo.contact;
|
||||
|
||||
when {
|
||||
contact.activeConn == null && contact.profile.contactLink != null -> ContactType.CARD
|
||||
contact.chatDeleted -> ContactType.CHAT_DELETED
|
||||
contact.contactStatus == ContactStatus.Active -> ContactType.RECENT
|
||||
else -> ContactType.UNLISTED
|
||||
}
|
||||
}
|
||||
else -> ContactType.UNLISTED
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterContactTypes(c: List<Chat>, contactTypes: List<ContactType>): List<Chat> {
|
||||
return c.filter { chat -> contactTypes.contains(chatContactType(chat)) }
|
||||
}
|
||||
|
||||
private var lazyListState = 0 to 0
|
||||
|
||||
@Composable
|
||||
private fun NewChatSheetLayout(
|
||||
newChatSheetState: StateFlow<AnimatedViewState>,
|
||||
stopped: Boolean,
|
||||
rh: RemoteHostInfo?,
|
||||
addContact: () -> Unit,
|
||||
scanPaste: () -> Unit,
|
||||
createGroup: () -> Unit,
|
||||
closeNewChatSheet: (animated: Boolean) -> Unit,
|
||||
close: () -> Unit,
|
||||
oneHandUI: SharedPreference<Boolean>
|
||||
) {
|
||||
var newChat by remember { mutableStateOf(newChatSheetState.value) }
|
||||
val resultingColor = if (isInDarkTheme()) Color.Black.copy(0.64f) else DrawerDefaults.scrimColor
|
||||
val animatedColor = remember {
|
||||
Animatable(
|
||||
if (newChat.isVisible()) Color.Transparent else resultingColor,
|
||||
Color.VectorConverter(resultingColor.colorSpace)
|
||||
)
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||
val searchShowingSimplexLink = remember { mutableStateOf(false) }
|
||||
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
|
||||
val showUnreadAndFavorites = remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value
|
||||
val baseContactTypes = listOf(ContactType.CARD, ContactType.RECENT, ContactType.REQUEST)
|
||||
val contactTypes by remember(baseContactTypes, searchText.value.text.isEmpty()) {
|
||||
derivedStateOf { contactTypesSearchTargets(baseContactTypes, searchText.value.text.isEmpty()) }
|
||||
}
|
||||
val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) }
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
newChatSheetState.collect {
|
||||
newChat = it
|
||||
launch {
|
||||
animatedColor.animateTo(if (newChat.isVisible()) resultingColor else Color.Transparent, newChatSheetAnimSpec())
|
||||
}
|
||||
launch {
|
||||
animatedFloat.animateTo(if (newChat.isVisible()) 1f else 0f, newChatSheetAnimSpec())
|
||||
if (newChat.isHiding()) closeNewChatSheet(false)
|
||||
val allChats by remember(chatModel.chats.value, contactTypes) {
|
||||
derivedStateOf { filterContactTypes(chatModel.chats.value, contactTypes) }
|
||||
}
|
||||
var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) }
|
||||
var previousIndex by remember { mutableStateOf(0) }
|
||||
var previousScrollOffset by remember { mutableStateOf(0) }
|
||||
|
||||
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
||||
val currentIndex = listState.firstVisibleItemIndex
|
||||
val currentScrollOffset = listState.firstVisibleItemScrollOffset
|
||||
val threshold = 25
|
||||
|
||||
scrollDirection = when {
|
||||
currentIndex > previousIndex -> ScrollDirection.Down
|
||||
currentIndex < previousIndex -> ScrollDirection.Up
|
||||
currentScrollOffset > previousScrollOffset + threshold -> ScrollDirection.Down
|
||||
currentScrollOffset < previousScrollOffset - threshold -> ScrollDirection.Up
|
||||
currentScrollOffset == previousScrollOffset -> ScrollDirection.Idle
|
||||
else -> scrollDirection
|
||||
}
|
||||
|
||||
previousIndex = currentIndex
|
||||
previousScrollOffset = currentScrollOffset
|
||||
}
|
||||
|
||||
val filteredContactChats = filteredContactChats(
|
||||
showUnreadAndFavorites = showUnreadAndFavorites,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchText = searchText.value.text,
|
||||
contactChats = allChats
|
||||
)
|
||||
|
||||
var sectionModifier = Modifier.fillMaxWidth()
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
sectionModifier = sectionModifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
listState
|
||||
) {
|
||||
stickyHeader {
|
||||
Column(
|
||||
Modifier
|
||||
.offset {
|
||||
val y = if (searchText.value.text.isEmpty()) {
|
||||
if (oneHandUI.state.value && scrollDirection == ScrollDirection.Up) {
|
||||
0
|
||||
} else if (listState.firstVisibleItemIndex == 0) -listState.firstVisibleItemScrollOffset else -1000
|
||||
} else {
|
||||
0
|
||||
}
|
||||
IntOffset(0, y)
|
||||
}
|
||||
.background(MaterialTheme.colors.background)
|
||||
) {
|
||||
if (!oneHandUI.state.value) {
|
||||
Divider()
|
||||
}
|
||||
ContactsSearchBar(
|
||||
listState = listState,
|
||||
searchText = searchText,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
close = close,
|
||||
oneHandUI = oneHandUI
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
val maxWidth = with(LocalDensity.current) { windowWidth() * density }
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(end = endPadding)
|
||||
.offset { IntOffset(if (newChat.isGone()) -maxWidth.value.roundToInt() else 0, 0) }
|
||||
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) { closeNewChatSheet(true) }
|
||||
.drawBehind { drawRect(animatedColor.value) },
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
val actions = remember { listOf(addContact, scanPaste, createGroup) }
|
||||
val backgroundColor = if (isInDarkTheme())
|
||||
blendARGB(MaterialTheme.colors.primary, Color.Black, 0.7F)
|
||||
else
|
||||
MaterialTheme.colors.background
|
||||
LazyColumn(Modifier
|
||||
.graphicsLayer {
|
||||
alpha = animatedFloat.value
|
||||
translationY = (1 - animatedFloat.value) * 20.dp.toPx()
|
||||
}) {
|
||||
items(actions.size) { index ->
|
||||
item {
|
||||
Spacer(Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
|
||||
if (searchText.value.text.isEmpty()) {
|
||||
Row {
|
||||
Spacer(Modifier.weight(1f))
|
||||
Box(contentAlignment = Alignment.CenterEnd) {
|
||||
Button(
|
||||
actions[index],
|
||||
shape = RoundedCornerShape(21.dp * fontSizeSqrtMultiplier),
|
||||
colors = ButtonDefaults.textButtonColors(backgroundColor = backgroundColor),
|
||||
elevation = null,
|
||||
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF),
|
||||
modifier = Modifier.height(42.dp * fontSizeSqrtMultiplier)
|
||||
) {
|
||||
Text(
|
||||
stringResource(titles[index]),
|
||||
Modifier.padding(start = DEFAULT_PADDING_HALF),
|
||||
color = if (isInDarkTheme()) MaterialTheme.colors.primary else MaterialTheme.colors.primary,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
Icon(
|
||||
painterResource(icons[index]),
|
||||
stringResource(titles[index]),
|
||||
Modifier.size(42.dp * fontSizeSqrtMultiplier),
|
||||
tint = if (isInDarkTheme()) MaterialTheme.colors.primary else MaterialTheme.colors.primary
|
||||
)
|
||||
SectionView {
|
||||
NewChatButton(
|
||||
icon = painterResource(MR.images.ic_add_link),
|
||||
text = stringResource(MR.strings.add_contact_tab),
|
||||
click = addContact,
|
||||
extraPadding = true,
|
||||
oneHandUI = oneHandUI.state
|
||||
)
|
||||
NewChatButton(
|
||||
icon = painterResource(MR.images.ic_qr_code),
|
||||
text = if (appPlatform.isAndroid) stringResource(MR.strings.scan_paste_link) else stringResource(MR.strings.paste_link),
|
||||
click = scanPaste,
|
||||
extraPadding = true,
|
||||
oneHandUI = oneHandUI.state
|
||||
)
|
||||
NewChatButton(
|
||||
icon = painterResource(MR.images.ic_group),
|
||||
text = stringResource(MR.strings.create_group_button),
|
||||
click = createGroup,
|
||||
extraPadding = true,
|
||||
oneHandUI = oneHandUI.state
|
||||
)
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
|
||||
val deletedContactTypes = listOf(ContactType.CHAT_DELETED)
|
||||
val deletedChats by remember(chatModel.chats.value, deletedContactTypes) {
|
||||
derivedStateOf { filterContactTypes(chatModel.chats.value, deletedContactTypes) }
|
||||
}
|
||||
if (deletedChats.isNotEmpty()) {
|
||||
Row(modifier = sectionModifier) {
|
||||
SectionView {
|
||||
SectionItemView(
|
||||
click = {
|
||||
ModalManager.start.showCustomModal { closeDeletedChats ->
|
||||
ModalView(
|
||||
close = closeDeletedChats,
|
||||
closeOnTop = !oneHandUI.state.value,
|
||||
closeBarTitle = if (oneHandUI.state.value) generalGetString(MR.strings.deleted_chats) else null,
|
||||
endButtons = { Spacer(Modifier.minimumInteractiveComponentSize()) }
|
||||
) {
|
||||
DeletedContactsView(rh = rh, close = {
|
||||
ModalManager.start.closeModals()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_folder_open),
|
||||
contentDescription = stringResource(MR.strings.deleted_chats),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
)
|
||||
TextIconSpaced(extraPadding = true)
|
||||
Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(DEFAULT_PADDING))
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
}
|
||||
}
|
||||
FloatingActionButton(
|
||||
onClick = { if (!stopped) closeNewChatSheet(true) },
|
||||
Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING).size(AppBarHeight * fontSizeSqrtMultiplier),
|
||||
elevation = FloatingActionButtonDefaults.elevation(
|
||||
defaultElevation = 0.dp,
|
||||
pressedElevation = 0.dp,
|
||||
hoveredElevation = 0.dp,
|
||||
focusedElevation = 0.dp,
|
||||
),
|
||||
backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
contentColor = Color.White
|
||||
|
||||
item {
|
||||
if (filteredContactChats.isNotEmpty() && !oneHandUI.state.value) {
|
||||
Text(
|
||||
stringResource(MR.strings.contact_list_header_title).uppercase(), color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2,
|
||||
modifier = sectionModifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(filteredContactChats) { index, chat ->
|
||||
val nextChatSelected = remember(chat.id, filteredContactChats) {
|
||||
derivedStateOf {
|
||||
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
}
|
||||
}
|
||||
ContactListNavLinkView(chat, nextChatSelected, oneHandUI.state)
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
|
||||
Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
generalGetString(MR.strings.no_filtered_contacts),
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewChatButton(
|
||||
icon: Painter,
|
||||
text: String,
|
||||
click: () -> Unit,
|
||||
textColor: Color = Color.Unspecified,
|
||||
iconColor: Color = MaterialTheme.colors.secondary,
|
||||
disabled: Boolean = false,
|
||||
extraPadding: Boolean = false,
|
||||
oneHandUI: State<Boolean>
|
||||
) {
|
||||
SectionItemView(click, disabled = disabled) {
|
||||
Row(modifier = if (oneHandUI.value) Modifier.scale(scaleX = 1f, scaleY = -1f) else Modifier) {
|
||||
Icon(icon, text, tint = if (disabled) MaterialTheme.colors.secondary else iconColor)
|
||||
TextIconSpaced(extraPadding)
|
||||
Text(text, color = if (disabled) MaterialTheme.colors.secondary else textColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactsSearchBar(
|
||||
listState: LazyListState,
|
||||
searchText: MutableState<TextFieldValue>,
|
||||
searchShowingSimplexLink: MutableState<Boolean>,
|
||||
searchChatFilteredBySimplexLink: MutableState<String?>,
|
||||
close: () -> Unit,
|
||||
oneHandUI: SharedPreference<Boolean>
|
||||
) {
|
||||
var modifier = Modifier.fillMaxWidth();
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
Icon(
|
||||
painterResource(MR.images.ic_search),
|
||||
contentDescription = null,
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(24.dp * fontSizeSqrtMultiplier),
|
||||
tint = MaterialTheme.colors.secondary
|
||||
)
|
||||
SearchTextField(
|
||||
Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester),
|
||||
placeholder = stringResource(MR.strings.search_or_paste_simplex_link),
|
||||
alwaysVisible = true,
|
||||
searchText = searchText,
|
||||
trailingContent = null,
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group),
|
||||
Modifier.graphicsLayer { alpha = 1 - animatedFloat.value }.size(24.dp * fontSizeSqrtMultiplier)
|
||||
)
|
||||
Icon(
|
||||
painterResource(MR.images.ic_close), stringResource(MR.strings.add_contact_or_create_group),
|
||||
Modifier.graphicsLayer { alpha = animatedFloat.value }.size(24.dp * fontSizeSqrtMultiplier)
|
||||
searchText.value = searchText.value.copy(it)
|
||||
}
|
||||
val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } }
|
||||
if (hasText.value) {
|
||||
val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() }
|
||||
BackHandler(onBack = hideSearchOnBack)
|
||||
KeyChangeEffect(chatModel.currentRemoteHost.value) {
|
||||
hideSearchOnBack()
|
||||
}
|
||||
} else {
|
||||
Row {
|
||||
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
|
||||
if (chatModel.chats.size > 0) {
|
||||
ToggleFilterButton()
|
||||
}
|
||||
Spacer(Modifier.width(padding))
|
||||
}
|
||||
}
|
||||
val focusManager = LocalFocusManager.current
|
||||
val keyboardState = getKeyboardState()
|
||||
LaunchedEffect(keyboardState.value) {
|
||||
if (keyboardState.value == KeyboardState.Closed && focused) {
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
val view = LocalMultiplatformView()
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { searchText.value.text }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
val link = strHasSingleSimplexLink(it.trim())
|
||||
if (link != null) {
|
||||
// if SimpleX link is pasted, show connection dialogue
|
||||
hideKeyboard(view)
|
||||
if (link.format is Format.SimplexLink) {
|
||||
val linkText =
|
||||
link.simplexLinkText(link.format.linkType, link.format.smpHosts)
|
||||
searchText.value =
|
||||
searchText.value.copy(linkText, selection = TextRange.Zero)
|
||||
}
|
||||
searchShowingSimplexLink.value = true
|
||||
searchChatFilteredBySimplexLink.value = null
|
||||
connect(
|
||||
link = link.text,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
close = close,
|
||||
cleanup = { searchText.value = TextFieldValue() }
|
||||
)
|
||||
} else if (!searchShowingSimplexLink.value || it.isEmpty()) {
|
||||
if (it.isNotEmpty()) {
|
||||
// if some other text is pasted, enter search mode
|
||||
focusRequester.requestFocus()
|
||||
} else if (listState.layoutInfo.totalItemsCount > 0) {
|
||||
listState.scrollToItem(0)
|
||||
}
|
||||
searchShowingSimplexLink.value = false
|
||||
searchChatFilteredBySimplexLink.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleFilterButton() {
|
||||
val pref = remember { ChatController.appPrefs.showUnreadAndFavorites }
|
||||
IconButton(onClick = { pref.set(!pref.get()) }) {
|
||||
val sp16 = with(LocalDensity.current) { 16.sp.toDp() }
|
||||
Icon(
|
||||
painterResource(MR.images.ic_filter_list),
|
||||
null,
|
||||
tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.padding(3.dp)
|
||||
.background(color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
|
||||
.border(width = 1.dp, color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
|
||||
.padding(3.dp)
|
||||
.size(sp16)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState<String?>, close: () -> Unit, cleanup: (() -> Unit)?) {
|
||||
withBGApi {
|
||||
planAndConnect(
|
||||
chatModel.remoteHostId(),
|
||||
URI.create(link),
|
||||
incognito = null,
|
||||
filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id },
|
||||
close = close,
|
||||
cleanup = cleanup,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun filteredContactChats(
|
||||
showUnreadAndFavorites: Boolean,
|
||||
searchShowingSimplexLink: State<Boolean>,
|
||||
searchChatFilteredBySimplexLink: State<String?>,
|
||||
searchText: String,
|
||||
contactChats: List<Chat>
|
||||
): List<Chat> {
|
||||
val linkChatId = searchChatFilteredBySimplexLink.value
|
||||
val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase()
|
||||
|
||||
return if (linkChatId != null) {
|
||||
contactChats.filter { it.id == linkChatId }
|
||||
} else {
|
||||
contactChats.filter { chat ->
|
||||
filterChat(
|
||||
chat = chat,
|
||||
searchText = s,
|
||||
showUnreadAndFavorites = showUnreadAndFavorites
|
||||
)
|
||||
}
|
||||
}
|
||||
.sortedWith(chatsByTypeComparator)
|
||||
}
|
||||
|
||||
private fun filterChat(chat: Chat, searchText: String, showUnreadAndFavorites: Boolean): Boolean {
|
||||
var meetsPredicate = true;
|
||||
val s = searchText.trim().lowercase()
|
||||
val cInfo = chat.chatInfo
|
||||
|
||||
if (searchText.isNotEmpty()) {
|
||||
meetsPredicate = viewNameContains(cInfo, s) ||
|
||||
if (cInfo is ChatInfo.Direct) (cInfo.contact.profile.displayName.lowercase().contains(s) ||
|
||||
cInfo.contact.fullName.lowercase().contains(s)) else false
|
||||
}
|
||||
|
||||
if (showUnreadAndFavorites) {
|
||||
meetsPredicate = meetsPredicate && (cInfo.chatSettings?.favorite ?: false)
|
||||
}
|
||||
|
||||
return meetsPredicate;
|
||||
}
|
||||
|
||||
private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean =
|
||||
cInfo.chatViewName.lowercase().contains(s.lowercase())
|
||||
|
||||
private val chatsByTypeComparator = Comparator<Chat> { chat1, chat2 ->
|
||||
val chat1Type = chatContactType(chat1)
|
||||
val chat2Type = chatContactType(chat2)
|
||||
|
||||
when {
|
||||
chat1Type.ordinal < chat2Type.ordinal -> -1
|
||||
chat1Type.ordinal > chat2Type.ordinal -> 1
|
||||
|
||||
else -> chat2.chatInfo.chatTs.compareTo(chat1.chatInfo.chatTs)
|
||||
}
|
||||
}
|
||||
|
||||
private fun contactTypesSearchTargets(baseContactTypes: List<ContactType>, searchEmpty: Boolean): List<ContactType> {
|
||||
return if (baseContactTypes.contains(ContactType.CHAT_DELETED) || searchEmpty) {
|
||||
baseContactTypes
|
||||
} else {
|
||||
baseContactTypes + ContactType.CHAT_DELETED
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeletedContactsView(rh: RemoteHostInfo?, close: () -> Unit) {
|
||||
val oneHandUI = remember { chatModel.controller.appPrefs.oneHandUI }
|
||||
|
||||
var modifier = Modifier.fillMaxSize()
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier,
|
||||
) {
|
||||
if (!oneHandUI.state.value) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
stringResource(MR.strings.deleted_chats),
|
||||
hostDevice(rh?.remoteHostId),
|
||||
bottomPadding = bottomPadding
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||
val searchShowingSimplexLink = remember { mutableStateOf(false) }
|
||||
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
|
||||
val showUnreadAndFavorites = remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value
|
||||
val contactTypes = listOf(ContactType.CHAT_DELETED)
|
||||
val allChats by remember(chatModel.chats.value, contactTypes) {
|
||||
derivedStateOf { filterContactTypes(chatModel.chats.value, contactTypes) }
|
||||
}
|
||||
val filteredContactChats = filteredContactChats(
|
||||
showUnreadAndFavorites = showUnreadAndFavorites,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchText = searchText.value.text,
|
||||
contactChats = allChats
|
||||
)
|
||||
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
listState
|
||||
) {
|
||||
item {
|
||||
Divider()
|
||||
ContactsSearchBar(
|
||||
listState = listState,
|
||||
searchText = searchText,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
close = close,
|
||||
oneHandUI = oneHandUI
|
||||
)
|
||||
Divider()
|
||||
|
||||
Spacer(Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
}
|
||||
|
||||
itemsIndexed(filteredContactChats) { index, chat ->
|
||||
val nextChatSelected = remember(chat.id, filteredContactChats) {
|
||||
derivedStateOf {
|
||||
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
}
|
||||
}
|
||||
ContactListNavLinkView(chat, nextChatSelected, oneHandUI.state)
|
||||
}
|
||||
}
|
||||
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
|
||||
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
generalGetString(MR.strings.no_filtered_contacts),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
modifier = if (oneHandUI.state.value) Modifier.scale(scaleX = 1f, scaleY = -1f) else Modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -267,13 +687,6 @@ fun ActionButton(
|
||||
@Composable
|
||||
private fun PreviewNewChatSheet() {
|
||||
SimpleXTheme {
|
||||
NewChatSheetLayout(
|
||||
MutableStateFlow(AnimatedViewState.VISIBLE),
|
||||
stopped = false,
|
||||
addContact = {},
|
||||
scanPaste = {},
|
||||
createGroup = {},
|
||||
closeNewChatSheet = {},
|
||||
)
|
||||
NewChatSheet(rh = null, close = {})
|
||||
}
|
||||
}
|
||||
|
||||
+13
-7
@@ -25,6 +25,7 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -62,7 +63,7 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC
|
||||
* It will be dropped automatically when connection established or when user goes away from this screen.
|
||||
* It applies only to Android because on Desktop center space will not be overlapped by [AddContactLearnMore]
|
||||
**/
|
||||
if (chatModel.showingInvitation.value != null && (!ModalManager.center.hasModalsOpen() || appPlatform.isDesktop)) {
|
||||
if (chatModel.showingInvitation.value != null && (ModalManager.start.openModalCount() == 1 || appPlatform.isDesktop)) {
|
||||
val conn = contactConnection.value
|
||||
if (chatModel.showingInvitation.value?.connChatUsed == false && conn != null) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
@@ -218,8 +219,10 @@ private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection
|
||||
withBGApi {
|
||||
val contactConn = contactConnection.value ?: return@withBGApi
|
||||
val conn = controller.apiSetConnectionIncognito(rhId, contactConn.pccConnId, incognito.value) ?: return@withBGApi
|
||||
contactConnection.value = conn
|
||||
chatModel.updateContactConnection(rhId, conn)
|
||||
withChats {
|
||||
contactConnection.value = conn
|
||||
updateContactConnection(rhId, conn)
|
||||
}
|
||||
}
|
||||
chatModel.markShowingInvitationUsed()
|
||||
}
|
||||
@@ -234,7 +237,8 @@ private fun AddContactLearnMoreButton() {
|
||||
ModalManager.end.showModalCloseable { close ->
|
||||
AddContactLearnMore(close)
|
||||
}
|
||||
}
|
||||
},
|
||||
Modifier.size(18.dp * fontSizeSqrtMultiplier)
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_info),
|
||||
@@ -367,9 +371,11 @@ private fun createInvitation(
|
||||
withBGApi {
|
||||
val (r, alert) = controller.apiAddContact(rhId, incognito = controller.appPrefs.incognito.get())
|
||||
if (r != null) {
|
||||
chatModel.updateContactConnection(rhId, r.second)
|
||||
chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false)
|
||||
contactConnection.value = r.second
|
||||
withChats {
|
||||
updateContactConnection(rhId, r.second)
|
||||
chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false)
|
||||
contactConnection.value = r.second
|
||||
}
|
||||
} else {
|
||||
creatingConnReq.value = false
|
||||
if (alert != null) {
|
||||
|
||||
+4
-1
@@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@@ -33,7 +34,9 @@ fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) {
|
||||
if (updated != null) {
|
||||
val (updatedProfile, updatedContacts) = updated
|
||||
m.updateCurrentUser(user.remoteHostId, updatedProfile, preferences)
|
||||
updatedContacts.forEach { m.updateContact(user.remoteHostId, it) }
|
||||
withChats {
|
||||
updatedContacts.forEach { updateContact(user.remoteHostId, it) }
|
||||
}
|
||||
currentPreferences = preferences
|
||||
}
|
||||
afterSave()
|
||||
|
||||
+24
-19
@@ -32,6 +32,7 @@ import chat.simplex.common.views.isValidDisplayName
|
||||
import chat.simplex.common.views.localauth.SetAppPasscodeView
|
||||
import chat.simplex.common.views.onboarding.ReadableText
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.*
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
@@ -121,14 +122,16 @@ fun PrivacySettingsView(
|
||||
chatModel.currentUser.value = currentUser.copy(sendRcptsContacts = enable)
|
||||
if (clearOverrides) {
|
||||
// For loop here is to prevent ConcurrentModificationException that happens with forEach
|
||||
for (i in 0 until chatModel.chats.size) {
|
||||
val chat = chatModel.chats[i]
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
var contact = chat.chatInfo.contact
|
||||
val sendRcpts = contact.chatSettings.sendRcpts
|
||||
if (sendRcpts != null && sendRcpts != enable) {
|
||||
contact = contact.copy(chatSettings = contact.chatSettings.copy(sendRcpts = null))
|
||||
chatModel.updateContact(currentUser.remoteHostId, contact)
|
||||
withChats {
|
||||
for (i in 0 until chats.size) {
|
||||
val chat = chats[i]
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
var contact = chat.chatInfo.contact
|
||||
val sendRcpts = contact.chatSettings.sendRcpts
|
||||
if (sendRcpts != null && sendRcpts != enable) {
|
||||
contact = contact.copy(chatSettings = contact.chatSettings.copy(sendRcpts = null))
|
||||
updateContact(currentUser.remoteHostId, contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,15 +146,17 @@ fun PrivacySettingsView(
|
||||
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
|
||||
chatModel.currentUser.value = currentUser.copy(sendRcptsSmallGroups = enable)
|
||||
if (clearOverrides) {
|
||||
// For loop here is to prevent ConcurrentModificationException that happens with forEach
|
||||
for (i in 0 until chatModel.chats.size) {
|
||||
val chat = chatModel.chats[i]
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
var groupInfo = chat.chatInfo.groupInfo
|
||||
val sendRcpts = groupInfo.chatSettings.sendRcpts
|
||||
if (sendRcpts != null && sendRcpts != enable) {
|
||||
groupInfo = groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(sendRcpts = null))
|
||||
chatModel.updateGroup(currentUser.remoteHostId, groupInfo)
|
||||
withChats {
|
||||
// For loop here is to prevent ConcurrentModificationException that happens with forEach
|
||||
for (i in 0 until chats.size) {
|
||||
val chat = chats[i]
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
var groupInfo = chat.chatInfo.groupInfo
|
||||
val sendRcpts = groupInfo.chatSettings.sendRcpts
|
||||
if (sendRcpts != null && sendRcpts != enable) {
|
||||
groupInfo = groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(sendRcpts = null))
|
||||
updateGroup(currentUser.remoteHostId, groupInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,7 +169,7 @@ fun PrivacySettingsView(
|
||||
DeliveryReceiptsSection(
|
||||
currentUser = currentUser,
|
||||
setOrAskSendReceiptsContacts = { enable ->
|
||||
val contactReceiptsOverrides = chatModel.chats.fold(0) { count, chat ->
|
||||
val contactReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat ->
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts
|
||||
count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1)
|
||||
@@ -179,7 +184,7 @@ fun PrivacySettingsView(
|
||||
}
|
||||
},
|
||||
setOrAskSendReceiptsGroups = { enable ->
|
||||
val groupReceiptsOverrides = chatModel.chats.fold(0) { count, chat ->
|
||||
val groupReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat ->
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts
|
||||
count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1)
|
||||
|
||||
@@ -318,7 +318,7 @@
|
||||
<string name="colored_text">ملون</string>
|
||||
<string name="status_contact_has_e2e_encryption">لدى جهة الاتصال التعمية بين الطريفين</string>
|
||||
<string name="create_profile_button">إنشاء</string>
|
||||
<string name="create_your_profile">إنشاء حسابك الشخصي</string>
|
||||
<string name="create_your_profile">إنشاء ملف تعريف</string>
|
||||
<string name="icon_descr_call_connecting">مكالمة جارية...</string>
|
||||
<string name="enable_self_destruct">تفعيل التدمير الذاتي</string>
|
||||
<string name="conn_event_ratchet_sync_started">الموافقة على التعمية…</string>
|
||||
@@ -354,7 +354,7 @@
|
||||
<string name="delete_group_question">حذف المجموعة؟</string>
|
||||
<string name="delete_link">حذف الرابط</string>
|
||||
<string name="share_text_deleted_at">حُذِفت في: %s</string>
|
||||
<string name="rcv_group_event_group_deleted">المجموعة المحذوفة</string>
|
||||
<string name="rcv_group_event_group_deleted">المجموعة حُذِفت</string>
|
||||
<string name="delete_image">حذف الصورة</string>
|
||||
<string name="v5_1_custom_themes">تخصيص السمات</string>
|
||||
<string name="delete_database">حذف قاعدة البيانات</string>
|
||||
@@ -1403,7 +1403,7 @@
|
||||
<string name="block_member_desc">سيتم إخفاء كافة الرسائل الجديدة من %s!</string>
|
||||
<string name="blocked_item_description">محظور</string>
|
||||
<string name="v5_4_block_group_members">حظر أعضاء المجموعة</string>
|
||||
<string name="rcv_direct_event_contact_deleted">جهة الاتصال المحذوفة</string>
|
||||
<string name="rcv_direct_event_contact_deleted">جهة الاتصال حُذِفت</string>
|
||||
<string name="v5_4_incognito_groups_descr">أنشِئ مجموعة باستخدام ملف تعريف عشوائي.</string>
|
||||
<string name="create_group_button">أنشِئ مجموعة</string>
|
||||
<string name="create_another_profile_button">أنشِئ ملف تعريف</string>
|
||||
@@ -1889,7 +1889,7 @@
|
||||
<string name="smp_servers_other">خوادم SMP أخرى</string>
|
||||
<string name="xftp_servers_configured">خوادم XFTP المهيأة</string>
|
||||
<string name="xftp_servers_other">خوادم XFTP أخرى</string>
|
||||
<string name="subscription_percentage">نسبة الاشتراك</string>
|
||||
<string name="subscription_percentage">أظهِر النسبة المئوية</string>
|
||||
<string name="app_check_for_updates_disabled">مُعطّل</string>
|
||||
<string name="app_check_for_updates_stable">مستقرّ</string>
|
||||
<string name="app_check_for_updates_update_available">يتوفر تحديث: %s</string>
|
||||
@@ -1947,15 +1947,15 @@
|
||||
<string name="app_check_for_updates">التمس التحديثات</string>
|
||||
<string name="acknowledgement_errors">أخطاء معترف بها</string>
|
||||
<string name="app_check_for_updates_download_completed_title">نُزّل تحديث التطبيق</string>
|
||||
<string name="all_users">جميع المستخدمين</string>
|
||||
<string name="all_users">جميع ملفات التعريف</string>
|
||||
<string name="attempts_label">المحاولات</string>
|
||||
<string name="app_check_for_updates_beta">تجريبي</string>
|
||||
<string name="chunks_uploaded">رُفع القطع</string>
|
||||
<string name="servers_info_sessions_connected">متصل</string>
|
||||
<string name="servers_info_connected_servers_section_header">الخوادم المتصلة</string>
|
||||
<string name="servers_info_sessions_connecting">جارِ الاتصال</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">الاتصالات المشتركة</string>
|
||||
<string name="current_user">المستخدم الحالي</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">الاتصالات النسطة</string>
|
||||
<string name="current_user">ملف التعريف الحالي</string>
|
||||
<string name="deletion_errors">أخطاء الحذف</string>
|
||||
<string name="servers_info_detailed_statistics">إحصائيات مفصلة</string>
|
||||
<string name="app_check_for_updates_notice_disable">عطّل</string>
|
||||
@@ -1969,7 +1969,7 @@
|
||||
<string name="app_check_for_updates_button_install">ثبّت التحديث</string>
|
||||
<string name="member_inactive_desc">قد يتم تسليم الرسالة لاحقًا إذا أصبح العضو نشطًا.</string>
|
||||
<string name="servers_info_messages_received">الرسائل المُستلمة</string>
|
||||
<string name="servers_info_subscriptions_section_header">اشتراكات الرسائل</string>
|
||||
<string name="servers_info_subscriptions_section_header">استقبال الرسائل</string>
|
||||
<string name="servers_info_missing">لا توجد معلومات، حاول إعادة التحميل</string>
|
||||
<string name="other_errors">أخطاء أخرى</string>
|
||||
<string name="servers_info_subscriptions_connections_pending">قيد الانتظار</string>
|
||||
@@ -1987,4 +1987,15 @@
|
||||
<string name="app_check_for_updates_notice_desc">لكي يتم إعلامك بالإصدارات الجديدة، شغّل الفحص الدوري للإصدارات المستقرة أو التجريبية.</string>
|
||||
<string name="servers_info_proxied_servers_section_footer">أنت غير متصل بهذه الخوادم. يتم استخدام التوجيه الخاص لتسليم الرسائل إليهم.</string>
|
||||
<string name="appearance_zoom">قرّب</string>
|
||||
<string name="smp_proxy_error_connecting">حدث خطأ أثناء الاتصال بخادم التحويل %1$s. يُرجى المحاولة لاحقا.</string>
|
||||
<string name="smp_proxy_error_broker_host">عنوان خادم التحويل غير متوافق مع إعدادات الشبكة: %1$s.</string>
|
||||
<string name="proxy_destination_error_broker_host">عنوان خادم الوجهة %1$s غير متوافق مع إعدادات خادم التحويل %2$s.</string>
|
||||
<string name="proxy_destination_error_broker_version">إصدار الخادم الوجهة %1$s غير متوافق مع خادم التحويل %2$s.</string>
|
||||
<string name="proxy_destination_error_failed_to_connect">فشل خادم التحويل %1$s في الاتصال بالخادم الوجهة %2$s. يُرجى المحاولة لاحقا.</string>
|
||||
<string name="smp_proxy_error_broker_version">إصدار خادم التحويل غير متوافق مع إعدادات الشبكة: %1$s.</string>
|
||||
<string name="privacy_media_blur_radius_off">مطفي</string>
|
||||
<string name="privacy_media_blur_radius_strong">قوي</string>
|
||||
<string name="privacy_media_blur_radius">تمويه الوسائط</string>
|
||||
<string name="privacy_media_blur_radius_medium">متوسط</string>
|
||||
<string name="privacy_media_blur_radius_soft">ناعم</string>
|
||||
</resources>
|
||||
@@ -351,6 +351,7 @@
|
||||
<string name="welcome">Welcome!</string>
|
||||
<string name="this_text_is_available_in_settings">This text is available in settings</string>
|
||||
<string name="your_chats">Chats</string>
|
||||
<string name="toolbar_settings">Settings</string>
|
||||
<string name="contact_connection_pending">connecting…</string>
|
||||
<string name="member_contact_send_direct_message">send direct message</string>
|
||||
<string name="group_preview_you_are_invited">you are invited to group</string>
|
||||
@@ -442,10 +443,25 @@
|
||||
<string name="notifications">Notifications</string>
|
||||
|
||||
<!-- Chat Info Actions - ChatInfoView.kt -->
|
||||
<string name="info_view_connect_button">connect</string>
|
||||
<string name="info_view_open_button">open</string>
|
||||
<string name="info_view_message_button">message</string>
|
||||
<string name="info_view_call_button">call</string>
|
||||
<string name="info_view_search_button">search</string>
|
||||
<string name="info_view_video_button">video</string>
|
||||
<string name="delete_contact_question">Delete contact?</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Contact and all messages will be deleted - this cannot be undone!</string>
|
||||
<string name="delete_contact_cannot_undo_warning">Contact will be deleted - this cannot be undone!</string>
|
||||
<string name="keep_conversation">Keep conversation</string>
|
||||
<string name="only_delete_conversation">Only delete conversation</string>
|
||||
<string name="confirm_delete_contact_question">Confirm contact deletion?</string>
|
||||
<string name="delete_and_notify_contact">Delete and notify contact</string>
|
||||
<string name="delete_without_notification">Delete without notification</string>
|
||||
<string name="button_delete_contact">Delete contact</string>
|
||||
<string name="conversation_deleted">Conversation deleted!</string>
|
||||
<string name="you_can_still_send_messages_to_contact">You can still send messages to %1$s from the Deleted chats.</string>
|
||||
<string name="contact_deleted">Contact deleted!</string>
|
||||
<string name="you_can_still_view_conversation_with_contact">You can still view conversation with %1$s in the list of chats.</string>
|
||||
<string name="text_field_set_contact_placeholder">Set contact name…</string>
|
||||
<string name="icon_descr_server_status_connected">Connected</string>
|
||||
<string name="icon_descr_server_status_disconnected">Disconnected</string>
|
||||
@@ -633,6 +649,7 @@
|
||||
<string name="new_chat">New chat</string>
|
||||
<string name="add_contact_tab">Add contact</string>
|
||||
<string name="scan_paste_link">Scan / Paste link</string>
|
||||
<string name="paste_link">Paste link</string>
|
||||
<string name="one_time_link">One-time invitation link</string>
|
||||
<string name="one_time_link_short">1-time link</string>
|
||||
<string name="simplex_address">SimpleX address</string>
|
||||
@@ -651,6 +668,10 @@
|
||||
<string name="invalid_qr_code">Invalid QR code</string>
|
||||
<string name="code_you_scanned_is_not_simplex_link_qr_code">The code you scanned is not a SimpleX link QR code.</string>
|
||||
|
||||
<string name="deleted_chats">Deleted chats</string>
|
||||
<string name="no_filtered_contacts">No filtered contacts</string>
|
||||
<string name="contact_list_header_title">Your contacts</string>
|
||||
|
||||
<!-- ScanCodeView.kt -->
|
||||
<string name="scan_code">Scan code</string>
|
||||
<string name="incorrect_code">Incorrect security code!</string>
|
||||
@@ -1123,6 +1144,7 @@
|
||||
<string name="settings_developer_tools">Developer tools</string>
|
||||
<string name="settings_experimental_features">Experimental features</string>
|
||||
<string name="settings_section_title_socks">SOCKS PROXY</string>
|
||||
<string name="settings_section_title_interface" translatable="false">INTERFACE</string>
|
||||
<string name="settings_section_title_language" translatable="false">LANGUAGE</string>
|
||||
<string name="settings_section_title_icon">APP ICON</string>
|
||||
<string name="settings_section_title_themes">THEMES</string>
|
||||
@@ -1258,6 +1280,7 @@
|
||||
<string name="database_downgrade">Database downgrade</string>
|
||||
<string name="incompatible_database_version">Incompatible database version</string>
|
||||
<string name="confirm_database_upgrades">Confirm database upgrades</string>
|
||||
<string name="one_hand_ui">One-hand UI</string>
|
||||
<string name="terminal_always_visible">Show console in new window</string>
|
||||
<string name="chat_list_always_visible">Show chat list in new window</string>
|
||||
<string name="invalid_migration_confirmation">Invalid migration confirmation</string>
|
||||
@@ -1450,6 +1473,7 @@
|
||||
<string name="send_receipts_disabled">disabled</string>
|
||||
<string name="send_receipts_disabled_alert_title">Receipts are disabled</string>
|
||||
<string name="send_receipts_disabled_alert_msg">This group has over %1$d members, delivery receipts are not sent.</string>
|
||||
<string name="action_button_add_members">Invite</string>
|
||||
|
||||
<!-- Chat / Chat item info -->
|
||||
<string name="section_title_for_console">FOR CONSOLE</string>
|
||||
@@ -1527,6 +1551,17 @@
|
||||
<string name="message_queue_info_none">none</string>
|
||||
<string name="message_queue_info_server_info">server queue info: %1$s\n\nlast received msg: %2$s</string>
|
||||
|
||||
<string name="cant_call_contact_alert_title">Can\'t call contact</string>
|
||||
<string name="cant_call_contact_connecting_wait_alert_text">Connecting to contact, please wait or check later!</string>
|
||||
<string name="cant_call_contact_deleted_alert_text">Contact is deleted.</string>
|
||||
<string name="allow_calls_question">Allow calls?</string>
|
||||
<string name="you_need_to_allow_calls">You need to allow your contact to call to be able to call them.</string>
|
||||
<string name="calls_prohibited_alert_title">Calls prohibited!</string>
|
||||
<string name="calls_prohibited_ask_to_enable_calls_alert_text">Please ask your contact to enable calls.</string>
|
||||
<string name="cant_call_member_alert_title">Can\'t call group member</string>
|
||||
<string name="cant_call_member_send_message_alert_text">Send message to enable calls.</string>
|
||||
<string name="cant_send_message_to_member_alert_title">Can\'t message group member</string>
|
||||
|
||||
<!-- GroupWelcomeView.kt -->
|
||||
<string name="group_welcome_title">Welcome message</string>
|
||||
<string name="save_welcome_message_question">Save welcome message?</string>
|
||||
|
||||
@@ -218,7 +218,7 @@
|
||||
<string name="notifications">Benachrichtigungen</string>
|
||||
<!-- Chat Info Actions - ChatInfoView.kt -->
|
||||
<string name="delete_contact_question">Kontakt löschen?</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Der Kontakt und alle Nachrichten werden gelöscht. Dies kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Es wird der Kontakt und alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="button_delete_contact">Kontakt löschen</string>
|
||||
<string name="text_field_set_contact_placeholder">Kontaktname festlegen…</string>
|
||||
<string name="icon_descr_server_status_connected">Verbunden</string>
|
||||
@@ -278,7 +278,7 @@
|
||||
<string name="reject_contact_button">Ablehnen</string>
|
||||
<!-- Clear Chat - ChatListNavLinkView.kt -->
|
||||
<string name="clear_chat_question">Chatinhalte löschen?</string>
|
||||
<string name="clear_chat_warning">Alle Nachrichten werden gelöscht. Dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht.</string>
|
||||
<string name="clear_chat_warning">Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht.</string>
|
||||
<string name="clear_verb">Löschen</string>
|
||||
<string name="clear_chat_button">Chatinhalte löschen</string>
|
||||
<string name="clear_chat_menu_action">Chatinhalte löschen</string>
|
||||
@@ -593,20 +593,20 @@
|
||||
<string name="error_exporting_chat_database">Fehler beim Exportieren der Chat-Datenbank</string>
|
||||
<string name="import_database_question">Chat-Datenbank importieren?</string>
|
||||
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Ihre aktuelle Chat-Datenbank wird GELÖSCHT und durch die importierte ERSETZT.
|
||||
\nDiese Aktion kann nicht rückgängig gemacht werden – Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string>
|
||||
\nDiese Aktion kann nicht rückgängig gemacht werden! Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string>
|
||||
<string name="import_database_confirmation">Importieren</string>
|
||||
<string name="error_deleting_database">Fehler beim Löschen der Chat-Datenbank</string>
|
||||
<string name="error_importing_database">Fehler beim Importieren der Chat-Datenbank</string>
|
||||
<string name="chat_database_imported">Chat-Datenbank importiert</string>
|
||||
<string name="restart_the_app_to_use_imported_chat_database">Starten Sie die App neu, um die importierte Chat-Datenbank zu verwenden.</string>
|
||||
<string name="delete_chat_profile_question">Chat-Profil löschen?</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">Diese Aktion kann nicht rückgängig gemacht werden – Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">Diese Aktion kann nicht rückgängig gemacht werden! Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string>
|
||||
<string name="chat_database_deleted">Chat-Datenbank gelöscht</string>
|
||||
<string name="restart_the_app_to_create_a_new_chat_profile">Starten Sie die App neu, um ein neues Chat-Profil zu erstellen.</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">Sie dürfen die neueste Version Ihrer Chat-Datenbank NUR auf einem Gerät verwenden, andernfalls erhalten Sie möglicherweise keine Nachrichten mehr von einigen Ihrer Kontakte.</string>
|
||||
<string name="stop_chat_to_enable_database_actions">Chat beenden, um Datenbankaktionen zu erlauben.</string>
|
||||
<string name="delete_files_and_media_question">Dateien und Medien löschen?</string>
|
||||
<string name="delete_files_and_media_desc">Diese Aktion kann nicht rückgängig gemacht werden – alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten.</string>
|
||||
<string name="delete_files_and_media_desc">Diese Aktion kann nicht rückgängig gemacht werden! Es werden alle empfangenen und gesendeten Dateien und Medien gelöscht. Bilder mit niedriger Auflösung bleiben erhalten.</string>
|
||||
<string name="no_received_app_files">Keine empfangenen oder gesendeten Dateien</string>
|
||||
<string name="total_files_count_and_size">%d Datei(en) mit einem Gesamtspeicherverbrauch von %s</string>
|
||||
<string name="chat_item_ttl_none">nie</string>
|
||||
@@ -616,7 +616,7 @@
|
||||
<string name="chat_item_ttl_seconds">%s Sekunde(n)</string>
|
||||
<string name="delete_messages_after">Löschen der Nachrichten</string>
|
||||
<string name="enable_automatic_deletion_question">Automatisches Löschen von Nachrichten aktivieren?</string>
|
||||
<string name="enable_automatic_deletion_message">Diese Aktion kann nicht rückgängig gemacht werden – alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dieser Vorgang kann mehrere Minuten dauern.</string>
|
||||
<string name="enable_automatic_deletion_message">Diese Aktion kann nicht rückgängig gemacht werden! Es werden alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, gelöscht. Dieser Vorgang kann mehrere Minuten dauern.</string>
|
||||
<string name="delete_messages">Nachrichten löschen</string>
|
||||
<string name="error_changing_message_deletion">Fehler beim Ändern der Einstellung</string>
|
||||
<!-- DatabaseEncryptionView.kt -->
|
||||
@@ -667,7 +667,7 @@
|
||||
<string name="database_backup_can_be_restored">Der Versuch, das Passwort der Datenbank zu ändern, konnte nicht abgeschlossen werden.</string>
|
||||
<string name="restore_database">Datenbanksicherung wiederherstellen</string>
|
||||
<string name="restore_database_alert_title">Datenbanksicherung wiederherstellen?</string>
|
||||
<string name="restore_database_alert_desc">Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden.</string>
|
||||
<string name="restore_database_alert_desc">Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="restore_database_alert_confirm">Wiederherstellen</string>
|
||||
<string name="database_restore_error">Fehler bei der Wiederherstellung der Datenbank</string>
|
||||
<string name="restore_passphrase_not_found_desc">Das Passwort wurde nicht im Schlüsselbund gefunden. Bitte geben Sie es manuell ein. Das kann passieren, wenn Sie die App-Daten mit einem Backup-Programm wieder hergestellt haben. Bitte nehmen Sie Kontakt mit den Entwicklern auf, wenn das nicht der Fall ist.</string>
|
||||
@@ -989,7 +989,7 @@
|
||||
<string name="app_version_name">App-Version: v%s</string>
|
||||
<string name="core_version">Core Version: v%s</string>
|
||||
<string name="users_add">Profil hinzufügen</string>
|
||||
<string name="users_delete_all_chats_deleted">Alle Chats und Nachrichten werden gelöscht. Dies kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="users_delete_all_chats_deleted">Es werden alle Chats und Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="users_delete_profile_for">Chat-Profil löschen für</string>
|
||||
<string name="network_option_ping_count">PING-Zähler</string>
|
||||
<string name="update_network_session_mode_question">Transport-Isolations-Modus aktualisieren\?</string>
|
||||
@@ -1088,7 +1088,7 @@
|
||||
<string name="database_upgrade">Datenbank-Aktualisierung</string>
|
||||
<string name="mtr_error_different">Unterschiedlicher Migrationsstand in der App/Datenbank: %s / %s</string>
|
||||
<string name="downgrade_and_open_chat">Datenbank herabstufen und den Chat öffnen</string>
|
||||
<string name="incompatible_database_version">Inkompatible Datenbank-Version</string>
|
||||
<string name="incompatible_database_version">Datenbank-Version nicht kompatibel</string>
|
||||
<string name="database_downgrade_warning">Warnung: Sie könnten einige Daten verlieren!</string>
|
||||
<string name="database_downgrade">Datenbank auf alte Version herabstufen</string>
|
||||
<string name="developer_options">Datenbank-IDs und Transport-Isolationsoption.</string>
|
||||
@@ -1555,14 +1555,14 @@
|
||||
<string name="bad_desktop_address">Falsche Desktop-Adresse</string>
|
||||
<string name="devices">Geräte</string>
|
||||
<string name="disconnect_desktop_question">Desktop-Verbindung trennen?</string>
|
||||
<string name="desktop_app_version_is_incompatible">Desktop-App-Version %s ist mit dieser App nicht kompatibel.</string>
|
||||
<string name="desktop_app_version_is_incompatible">Die Desktop-App-Version %s ist nicht mit dieser App kompatibel.</string>
|
||||
<string name="new_mobile_device">Neues Mobiltelefon-Gerät</string>
|
||||
<string name="only_one_device_can_work_at_the_same_time">Nur ein Gerät kann gleichzeitig genutzt werden</string>
|
||||
<string name="v5_4_link_mobile_desktop">Verknüpfe Mobiltelefon- und Desktop-Apps! 🔗</string>
|
||||
<string name="v5_4_link_mobile_desktop_descr">Über ein sicheres quantenbeständiges Protokoll</string>
|
||||
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Öffnen Sie in den Einstellungen der Mobiltelefon-App <i>Vom Desktop aus nutzen</i> und scannen Sie den QR-Code.]]></string>
|
||||
<string name="v5_4_block_group_members_descr">Um unerwünschte Nachrichten zu verbergen.</string>
|
||||
<string name="desktop_incompatible_version">Inkompatible Version</string>
|
||||
<string name="desktop_incompatible_version">Version nicht kompatibel</string>
|
||||
<string name="new_desktop"><![CDATA[<i>(Neu)</i>]]></string>
|
||||
<string name="unlink_desktop_question">Desktop entkoppeln?</string>
|
||||
<string name="linked_desktop_options">Verknüpfte Desktop-Optionen</string>
|
||||
@@ -1854,14 +1854,14 @@
|
||||
<string name="ci_status_other_error">Fehler: %1$s</string>
|
||||
<string name="message_delivery_warning_title">Warnung bei der Nachrichtenzustellung</string>
|
||||
<string name="snd_error_auth">Falscher Schlüssel oder unbekannte Verbindung - höchstwahrscheinlich ist diese Verbindung gelöscht.</string>
|
||||
<string name="srv_error_version">Die Server-Version ist nicht mit den Netzwerk-Einstellungen kompatibel.</string>
|
||||
<string name="srv_error_version">Die Server-Version ist nicht mit den Netzwerkeinstellungen kompatibel.</string>
|
||||
<string name="snd_error_quota">Kapazität überschritten - der Empfänger hat die zuvor gesendeten Nachrichten nicht empfangen.</string>
|
||||
<string name="snd_error_proxy_relay">Weiterleitungsserver: %1$s
|
||||
\nFehler auf dem Zielserver: %2$s</string>
|
||||
<string name="snd_error_proxy">Weiterleitungsserver: %1$s
|
||||
\nFehler: %2$s</string>
|
||||
<string name="snd_error_expired">Netzwerk-Fehler - die Nachricht ist nach vielen Sende-Versuchen abgelaufen.</string>
|
||||
<string name="srv_error_host">Die Server-Adresse ist nicht mit den Netzwerk-Einstellungen kompatibel.</string>
|
||||
<string name="srv_error_host">Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel.</string>
|
||||
<string name="network_smp_proxy_mode_always">Immer</string>
|
||||
<string name="network_smp_proxy_mode_private_routing">Privates Routing</string>
|
||||
<string name="network_smp_proxy_mode_never">Nie</string>
|
||||
@@ -1967,8 +1967,8 @@
|
||||
<string name="member_info_member_inactive">Inaktiv</string>
|
||||
<string name="servers_info_sessions_connected">Verbunden</string>
|
||||
<string name="servers_info_sessions_connecting">Verbinden</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">Abonnierte Verbindungen</string>
|
||||
<string name="current_user">Aktueller Nutzer</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">Aktive Verbindungen</string>
|
||||
<string name="current_user">Aktuelles Profil</string>
|
||||
<string name="servers_info_detailed_statistics">Detaillierte Statistiken</string>
|
||||
<string name="servers_info_details">Details</string>
|
||||
<string name="servers_info_downloaded">Heruntergeladen</string>
|
||||
@@ -1977,7 +1977,7 @@
|
||||
<string name="servers_info_reset_stats_alert_error_title">Fehler beim Zurücksetzen der Statistiken</string>
|
||||
<string name="servers_info_sessions_errors">Fehler</string>
|
||||
<string name="servers_info_messages_received">Empfangene Nachrichten</string>
|
||||
<string name="servers_info_subscriptions_section_header">Nachrichten-Abonnements</string>
|
||||
<string name="servers_info_subscriptions_section_header">Nachrichtenempfang</string>
|
||||
<string name="servers_info_subscriptions_connections_pending">Ausstehend</string>
|
||||
<string name="servers_info_previously_connected_servers_section_header">Bisher verbundene Server</string>
|
||||
<string name="servers_info_proxied_servers_section_header">Proxy-Server</string>
|
||||
@@ -2010,7 +2010,7 @@
|
||||
<string name="app_check_for_updates_button_install">Aktualisierung installieren</string>
|
||||
<string name="app_check_for_updates_installed_successfully_desc">Bitte starten Sie die App neu.</string>
|
||||
<string name="acknowledged">Bestätigt</string>
|
||||
<string name="all_users">Alle Nutzer</string>
|
||||
<string name="all_users">Alle Profile</string>
|
||||
<string name="app_check_for_updates_download_completed_title">App-Aktualisierung wurde heruntergeladen</string>
|
||||
<string name="app_check_for_updates">Nach Aktualisierungen suchen</string>
|
||||
<string name="chunks_downloaded">Daten-Pakete heruntergeladen</string>
|
||||
@@ -2026,7 +2026,7 @@
|
||||
<string name="servers_info_missing">Keine Information - es wird versucht neu zu laden</string>
|
||||
<string name="app_check_for_updates_button_open">Dateispeicherort öffnen</string>
|
||||
<string name="other_label">andere</string>
|
||||
<string name="network_error_broker_host_desc">Die Server-Adresse ist nicht mit den Netzwerk-Einstellungen kompatibel: %1$s.</string>
|
||||
<string name="network_error_broker_host_desc">Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel: %1$s.</string>
|
||||
<string name="scan_paste_link">Link scannen / einfügen</string>
|
||||
<string name="servers_info_reconnect_all_servers_button">Alle Server neu verbinden</string>
|
||||
<string name="servers_info_reconnect_server_title">Server neu verbinden?</string>
|
||||
@@ -2045,13 +2045,13 @@
|
||||
<string name="server_address">Server-Adresse</string>
|
||||
<string name="app_check_for_updates_button_remind_later">Später erinnern</string>
|
||||
<string name="servers_info">Server-Informationen</string>
|
||||
<string name="network_error_broker_version_desc">Ihre App ist nicht kompatibel mit der Server-Version: %1$s.</string>
|
||||
<string name="subscription_percentage">Prozentualer Anteil der Abonnements</string>
|
||||
<string name="network_error_broker_version_desc">Ihre App ist nicht mit der Server-Version kompatibel: %1$s.</string>
|
||||
<string name="subscription_percentage">Prozentualen Anteil anzeigen</string>
|
||||
<string name="appearance_zoom">Zoom</string>
|
||||
<string name="smp_server">SMP-Server</string>
|
||||
<string name="servers_info_target">Informationen zeigen für</string>
|
||||
<string name="servers_info_private_data_disclaimer">Beginnend mit %s.
|
||||
\nAlle Daten sind auf Ihrem Gerät geschützt.</string>
|
||||
\nAlle Daten werden nur auf Ihrem Gerät gespeichert.</string>
|
||||
<string name="servers_info_statistics_section_header">Statistiken</string>
|
||||
<string name="servers_info_transport_sessions_section_header">Transport-Sitzungen</string>
|
||||
<string name="servers_info_uploaded">Hochgeladen</string>
|
||||
@@ -2071,4 +2071,15 @@
|
||||
<string name="app_check_for_updates_update_available">Aktualisierung verfügbar: %s</string>
|
||||
<string name="app_check_for_updates_notice_desc">Aktivieren Sie die periodische Überprüfung auf stabile oder Beta-Versionen der App, um über neue Versionen benachrichtigt zu werden.</string>
|
||||
<string name="app_check_for_updates_canceled">Herunterladen der Aktualisierung abgebrochen</string>
|
||||
<string name="smp_proxy_error_broker_host">Die Weiterleitungsserver-Adresse ist nicht kompatibel mit den Netzwerkeinstellungen: %1$s.</string>
|
||||
<string name="proxy_destination_error_failed_to_connect">Die Verbindung des Weiterleitungsservers %1$s zum Zielserver %2$s schlug fehl. Bitte versuchen Sie es später erneut.</string>
|
||||
<string name="proxy_destination_error_broker_version">Die Zielserver-Version von %1$s ist nicht mit dem Weiterleitungsserver %2$s kompatibel.</string>
|
||||
<string name="smp_proxy_error_broker_version">Die Weiterleitungsserver-Version ist nicht kompatibel mit den Netzwerkeinstellungen: %1$s.</string>
|
||||
<string name="proxy_destination_error_broker_host">Die Zielserver-Adresse von %1$s ist nicht mit den Einstellungen des Weiterleitungsservers %2$s kompatibel.</string>
|
||||
<string name="smp_proxy_error_connecting">Fehler beim Verbinden zum Weiterleitungsserver %1$s. Bitte versuchen Sie es später erneut.</string>
|
||||
<string name="privacy_media_blur_radius">Medium unscharf machen</string>
|
||||
<string name="privacy_media_blur_radius_soft">Weich</string>
|
||||
<string name="privacy_media_blur_radius_strong">Hart</string>
|
||||
<string name="privacy_media_blur_radius_medium">Medium</string>
|
||||
<string name="privacy_media_blur_radius_off">Aus</string>
|
||||
</resources>
|
||||
@@ -1777,7 +1777,7 @@
|
||||
<string name="snd_error_proxy">Servidor de reenvío: %1$s
|
||||
\nError: %2$s</string>
|
||||
<string name="snd_error_expired">Problema en la red - el mensaje ha expirado tras muchos intentos de envío.</string>
|
||||
<string name="srv_error_version">La versión del servidor es incompatible con la configuración de red.</string>
|
||||
<string name="srv_error_version">La versión del servidor es incompatible con la configuración de la red.</string>
|
||||
<string name="network_smp_proxy_mode_private_routing">Enrutamiento privado</string>
|
||||
<string name="network_smp_proxy_mode_unknown">Con servidores desconocidos</string>
|
||||
<string name="network_smp_proxy_mode_never_description">NO usar enrutamiento privado.</string>
|
||||
@@ -1841,7 +1841,7 @@
|
||||
<string name="message_queue_info_server_info">información cola del servidor: %1$s
|
||||
\n
|
||||
\núltimo mensaje recibido: %2$s</string>
|
||||
<string name="chat_theme_reset_to_app_theme">Restablecer al tema de la app</string>
|
||||
<string name="chat_theme_reset_to_app_theme">Restablecer al tema de la aplicación</string>
|
||||
<string name="v5_8_private_routing">Enrutamiento privado de mensajes 🚀</string>
|
||||
<string name="v5_8_safe_files">Recibe archivos de forma segura</string>
|
||||
<string name="v5_8_message_delivery">Mejora del envío de mensajes</string>
|
||||
@@ -1859,19 +1859,135 @@
|
||||
<string name="error_initializing_web_view">Error al inicializar WebView. Actualiza tu sistema a la última versión. Por favor, ponte en contacto con los desarrolladores.
|
||||
\nError: %s</string>
|
||||
<string name="v5_8_persian_ui">Interfaz en persa</string>
|
||||
<string name="file_error_auth">Clave incorrecta o dirección de bloque de datos del archivo desconocida - lo más probable es que el archivo se haya borrado.</string>
|
||||
<string name="file_error_no_file">Archivo no encontrado - el archivo probablemente ha sido borrado o cancelado.</string>
|
||||
<string name="file_error_auth">Clave incorrecta o dirección del bloque del archivo desconocida. Es probable que el archivo se haya eliminado.</string>
|
||||
<string name="file_error_no_file">Archivo no encontrado, probablemente haya sido borrado o cancelado.</string>
|
||||
<string name="file_error_relay">Error del servidor de archivos: %1$s</string>
|
||||
<string name="file_error">Error de archivo</string>
|
||||
<string name="temporary_file_error">Error de archivo temporal</string>
|
||||
<string name="temporary_file_error">Error en archivo temporal</string>
|
||||
<string name="share_text_message_status">Estado del mensaje: %s</string>
|
||||
<string name="info_row_message_status">Estado del mensaje</string>
|
||||
<string name="share_text_file_status">Estado del archivo: %s</string>
|
||||
<string name="info_row_file_status">Estado del archivo</string>
|
||||
<string name="remote_ctrl_connection_stopped_desc">Comprueba que el móvil y el ordenador están conectados a la misma red local y que el firewall del ordenador permite la conexión.
|
||||
<string name="remote_ctrl_connection_stopped_desc">Comprueba que el móvil y el ordenador están conectados a la misma red local y que el cortafuegos del ordenador permite la conexión.
|
||||
\nPor favor, comparte cualquier otro problema con los desarrolladores.</string>
|
||||
<string name="copy_error">Error al copiar</string>
|
||||
<string name="copy_error">Copiar error</string>
|
||||
<string name="remote_ctrl_connection_stopped_identity_desc">Este enlace ha sido usado en otro dispositivo móvil, por favor crea un enlace nuevo en el ordenador.</string>
|
||||
<string name="cannot_share_message_alert_title">No se puede enviar el mensaje</string>
|
||||
<string name="cannot_share_message_alert_text">Las preferencias seleccionadas no permiten este mensaje.</string>
|
||||
<string name="servers_info">Info servidores</string>
|
||||
<string name="servers_info_files_tab">Archivos</string>
|
||||
<string name="servers_info_target">Mostrando info de</string>
|
||||
<string name="subscribed">Suscrito</string>
|
||||
<string name="subscription_errors">Errores de suscripción</string>
|
||||
<string name="subscription_results_ignored">Suscripciones ignoradas</string>
|
||||
<string name="app_check_for_updates_notice_desc">Para ser notificado sobre versiones nuevas, activa el chequeo periódico para las versiones Estable o Beta.</string>
|
||||
<string name="app_check_for_updates_beta">Beta</string>
|
||||
<string name="smp_servers_configured">Servidores SMP configurados</string>
|
||||
<string name="xftp_servers_configured">Servidores XFTP configurados</string>
|
||||
<string name="servers_info_connected_servers_section_header">Servidores conectados</string>
|
||||
<string name="servers_info_sessions_connecting">Conectando</string>
|
||||
<string name="current_user">Perfil actual</string>
|
||||
<string name="appearance_zoom">Zoom</string>
|
||||
<string name="servers_info_uploaded">Subido</string>
|
||||
<string name="app_check_for_updates_update_available">Actualización disponible: %s</string>
|
||||
<string name="app_check_for_updates_canceled">Descarga de actualización cancelada</string>
|
||||
<string name="app_check_for_updates_download_completed_title">Actualización descargada</string>
|
||||
<string name="app_check_for_updates">Buscar actualizaciones</string>
|
||||
<string name="app_check_for_updates_notice_title">Buscar actualizaciones</string>
|
||||
<string name="servers_info_statistics_section_header">Estadísticas</string>
|
||||
<string name="servers_info_subscriptions_total">Total</string>
|
||||
<string name="servers_info_transport_sessions_section_header">Sesiones de transporte</string>
|
||||
<string name="xftp_server">Servidor XFTP</string>
|
||||
<string name="servers_info_proxied_servers_section_footer">No estás conectado a estos servidores. Para enviarles mensajes se usa el enrutamiento privado.</string>
|
||||
<string name="all_users">Todos los perfiles</string>
|
||||
<string name="servers_info_sessions_connected">Conectado</string>
|
||||
<string name="servers_info_detailed_statistics">Estadísticas detalladas</string>
|
||||
<string name="servers_info_details">Detalles</string>
|
||||
<string name="uploaded_files">Archivos subidos</string>
|
||||
<string name="upload_errors">Errores en subida</string>
|
||||
<string name="attempts_label">intentos</string>
|
||||
<string name="completed">Completado</string>
|
||||
<string name="connections">Conexiones</string>
|
||||
<string name="created">Creado</string>
|
||||
<string name="decryption_errors">errores de descifrado</string>
|
||||
<string name="deleted">Eliminado</string>
|
||||
<string name="deletion_errors">Errores de borrado</string>
|
||||
<string name="member_info_member_disabled">desactivado</string>
|
||||
<string name="message_forwarded_title">Mensaje reenviado</string>
|
||||
<string name="member_inactive_desc">El mensaje puede ser entregado más tarde si el miembro vuelve a estar activo.</string>
|
||||
<string name="member_inactive_title">Miembro inactivo</string>
|
||||
<string name="please_try_later">Por favor, inténtalo más tarde.</string>
|
||||
<string name="private_routing_error">Error de enrutamiento privado</string>
|
||||
<string name="network_error_broker_host_desc">La dirección del servidor es incompatible con la configuración de red: %1$s.</string>
|
||||
<string name="network_error_broker_version_desc">La versión del servidor es incompatible con tu aplicación: %1$s.</string>
|
||||
<string name="appearance_font_size">Tamaño fuente</string>
|
||||
<string name="servers_info_reset_stats_alert_error_title">Error al restablecer las estadísticas</string>
|
||||
<string name="servers_info_reset_stats_alert_confirm">Restablecer</string>
|
||||
<string name="servers_info_reset_stats_alert_message">Las estadísticas de los servidores serán restablecidas. ¡No podrá deshacerse!</string>
|
||||
<string name="servers_info_downloaded">Descargado</string>
|
||||
<string name="smp_server">Servidor SMP</string>
|
||||
<string name="message_forwarded_desc">Aún no hay conexión directa, el mensaje es reenviado por el administrador.</string>
|
||||
<string name="smp_servers_other">Otros servidores SMP</string>
|
||||
<string name="xftp_servers_other">Otros servidores XFTP</string>
|
||||
<string name="scan_paste_link">Escanear / Pegar enlace</string>
|
||||
<string name="subscription_percentage">Mostrar porcentaje</string>
|
||||
<string name="app_check_for_updates_notice_disable">Desactivar</string>
|
||||
<string name="app_check_for_updates_disabled">Desactivado</string>
|
||||
<string name="app_check_for_updates_download_started">Descargando actualización, por favor no cierres la aplicación</string>
|
||||
<string name="app_check_for_updates_button_download">Descarga %s (%s)</string>
|
||||
<string name="app_check_for_updates_installed_successfully_title">Instalación completada</string>
|
||||
<string name="app_check_for_updates_button_install">Instalar actualización</string>
|
||||
<string name="app_check_for_updates_button_open">Abrir ubicación del archivo</string>
|
||||
<string name="app_check_for_updates_installed_successfully_desc">Por favor, reinicia la aplicación.</string>
|
||||
<string name="app_check_for_updates_button_remind_later">Recordar más tarde</string>
|
||||
<string name="app_check_for_updates_button_skip">Saltar esta versión</string>
|
||||
<string name="app_check_for_updates_stable">Estable</string>
|
||||
<string name="member_info_member_inactive">inactivo</string>
|
||||
<string name="servers_info_modal_error_title">Error</string>
|
||||
<string name="servers_info_reconnect_server_error">Error al reconectar con el servidor</string>
|
||||
<string name="servers_info_reconnect_servers_error">Error al reconectar con los servidores</string>
|
||||
<string name="servers_info_sessions_errors">Errores</string>
|
||||
<string name="servers_info_subscriptions_section_header">Recepción de mensajes</string>
|
||||
<string name="servers_info_messages_received">Mensajes recibidos</string>
|
||||
<string name="servers_info_messages_sent">Mensajes enviados</string>
|
||||
<string name="servers_info_missing">Sin información, intenta recargar</string>
|
||||
<string name="servers_info_subscriptions_connections_pending">Pendiente</string>
|
||||
<string name="servers_info_previously_connected_servers_section_header">Servidores conectados previamente</string>
|
||||
<string name="servers_info_proxied_servers_section_header">Servidores con proxy</string>
|
||||
<string name="servers_info_detailed_statistics_received_messages_header">Mensajes recibidos</string>
|
||||
<string name="servers_info_detailed_statistics_received_total">Total recibido</string>
|
||||
<string name="servers_info_detailed_statistics_receive_errors">Errores de recepción</string>
|
||||
<string name="servers_info_reconnect_all_servers_button">Reconectar todos los servidores</string>
|
||||
<string name="servers_info_reconnect_server_title">¿Reconectar servidor?</string>
|
||||
<string name="servers_info_reconnect_servers_title">¿Reconectar servidores?</string>
|
||||
<string name="servers_info_reconnect_server_message">Reconectar el servidor para forzar la entrega de mensajes. Usa tráfico adicional.</string>
|
||||
<string name="servers_info_reconnect_servers_message">Reconectar todos los servidores para forzar la entrega de mensajes. Usa tráfico adicional.</string>
|
||||
<string name="servers_info_reset_stats">Restablecer estadísticas</string>
|
||||
<string name="servers_info_reset_stats_alert_title">¿Restablecer todas las estadísticas?</string>
|
||||
<string name="servers_info_detailed_statistics_sent_messages_header">Mensajes enviados</string>
|
||||
<string name="servers_info_detailed_statistics_sent_messages_total">Total enviado</string>
|
||||
<string name="downloaded_files">Archivos descargados</string>
|
||||
<string name="download_errors">Errores en la descarga</string>
|
||||
<string name="duplicates_label">duplicados</string>
|
||||
<string name="expired_label">expirado</string>
|
||||
<string name="open_server_settings_button">Abrir configuración del servidor</string>
|
||||
<string name="other_label">otro</string>
|
||||
<string name="other_errors">otros errores</string>
|
||||
<string name="proxied">Con proxy</string>
|
||||
<string name="reconnect">Reconectar</string>
|
||||
<string name="secured">Seguro</string>
|
||||
<string name="send_errors">Errores de envío</string>
|
||||
<string name="sent_directly">Enviado directamente</string>
|
||||
<string name="sent_via_proxy">Enviado mediante proxy</string>
|
||||
<string name="server_address">Dirección del servidor</string>
|
||||
<string name="size">Tamaño</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">Conexiones activas</string>
|
||||
<string name="servers_info_starting_from">Iniciado desde %s.</string>
|
||||
<string name="servers_info_private_data_disclaimer">Iniciado desde %s
|
||||
\nTodos los datos son privados a tu dispositivo</string>
|
||||
<string name="chunks_deleted">Bloques eliminados</string>
|
||||
<string name="chunks_downloaded">Bloques descargados</string>
|
||||
<string name="chunks_uploaded">Bloques subidos</string>
|
||||
<string name="acknowledged">Reconocido</string>
|
||||
<string name="acknowledgement_errors">Errores de reconocimiento</string>
|
||||
</resources>
|
||||
@@ -1135,7 +1135,7 @@
|
||||
<string name="share_address">Partager l\'adresse</string>
|
||||
<string name="you_can_share_this_address_with_your_contacts">Vous pouvez partager cette adresse avec vos contacts pour leur permettre de se connecter avec %s.</string>
|
||||
<string name="group_welcome_preview">Aperçu</string>
|
||||
<string name="color_background">Fond d\'écran</string>
|
||||
<string name="color_background">Fond</string>
|
||||
<string name="dark_theme">Thème sombre</string>
|
||||
<string name="export_theme">Exporter le thème</string>
|
||||
<string name="import_theme">Importer un thème</string>
|
||||
@@ -1873,4 +1873,120 @@
|
||||
<string name="remote_ctrl_connection_stopped_identity_desc">Ce lien a été utilisé avec un autre appareil mobile, veuillez créer un nouveau lien sur le desktop.</string>
|
||||
<string name="cannot_share_message_alert_title">Impossible d\'envoyer le message</string>
|
||||
<string name="cannot_share_message_alert_text">Les paramètres de chat sélectionnés ne permettent pas l\'envoi de ce message.</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">Connections actives</string>
|
||||
<string name="all_users">Tous les profiles</string>
|
||||
<string name="acknowledged">Reçu avec accusé de réception</string>
|
||||
<string name="app_check_for_updates_download_completed_title">La mise à jour de l\'app est téléchargée</string>
|
||||
<string name="completed">Terminé</string>
|
||||
<string name="current_user">Profil actuel</string>
|
||||
<string name="decryption_errors">Erreurs de déchiffrement</string>
|
||||
<string name="member_info_member_disabled">désactivé</string>
|
||||
<string name="deleted">Supprimé</string>
|
||||
<string name="app_check_for_updates_notice_disable">Désactiver</string>
|
||||
<string name="app_check_for_updates_download_started">Téléchargement de la mise à jour de l\'appli, ne pas fermer l\'appli</string>
|
||||
<string name="app_check_for_updates_button_download">Téléchargement %s (%s)</string>
|
||||
<string name="servers_info_reconnect_server_error">Erreur de reconnexion au serveur</string>
|
||||
<string name="member_info_member_inactive">inactif</string>
|
||||
<string name="scan_paste_link">Scanner / Coller le lien</string>
|
||||
<string name="member_inactive_desc">Le message peut être transmis plus tard si le membre devient actif.</string>
|
||||
<string name="servers_info_reconnect_servers_message">Reconnecter tous les serveurs connectés pour forcer la livraison des messages. Cette méthode utilise du trafic supplémentaire.</string>
|
||||
<string name="servers_info_transport_sessions_section_header">Sessions de transport</string>
|
||||
<string name="network_error_broker_host_desc">L\'adresse du serveur est incompatible avec les paramètres réseau : %1$s.</string>
|
||||
<string name="network_error_broker_version_desc">La version du serveur est incompatible avec votre application : %1$s.</string>
|
||||
<string name="private_routing_error">Erreur de routage privé</string>
|
||||
<string name="please_try_later">Veuillez essayer plus tard.</string>
|
||||
<string name="member_inactive_title">Membre inactif</string>
|
||||
<string name="message_forwarded_title">Message transféré</string>
|
||||
<string name="message_forwarded_desc">Pas de connexion directe pour l\'instant, le message est transmis par l\'administrateur.</string>
|
||||
<string name="servers_info_connected_servers_section_header">Serveurs connectés</string>
|
||||
<string name="servers_info_previously_connected_servers_section_header">Serveurs précédemment connectés</string>
|
||||
<string name="servers_info_proxied_servers_section_header">Serveurs routés via des proxy</string>
|
||||
<string name="servers_info_proxied_servers_section_footer">Vous n\'êtes pas connecté à ces serveurs. Le routage privé est utilisé pour leur délivrer des messages.</string>
|
||||
<string name="servers_info_reconnect_server_title">Reconnecter le serveur ?</string>
|
||||
<string name="servers_info_reconnect_servers_title">Reconnecter les serveurs ?</string>
|
||||
<string name="servers_info_reconnect_server_message">Reconnecter le serveur pour forcer la livraison des messages. Utilise du trafic supplémentaire.</string>
|
||||
<string name="servers_info_reset_stats_alert_error_title">Erreur de réinitialisation des statistiques</string>
|
||||
<string name="servers_info_reset_stats_alert_confirm">Réinitialiser</string>
|
||||
<string name="servers_info_reset_stats_alert_message">Les statistiques des serveurs seront réinitialisées - il n\'est pas possible de revenir en arrière !</string>
|
||||
<string name="servers_info_uploaded">Téléversé</string>
|
||||
<string name="servers_info_detailed_statistics">Statistiques détaillées</string>
|
||||
<string name="servers_info_detailed_statistics_sent_messages_header">Messages envoyés</string>
|
||||
<string name="smp_server">Serveur SMP</string>
|
||||
<string name="chunks_deleted">Chunks supprimés</string>
|
||||
<string name="chunks_downloaded">Chunks téléchargés</string>
|
||||
<string name="downloaded_files">Fichiers téléchargés</string>
|
||||
<string name="download_errors">Erreurs de téléchargement</string>
|
||||
<string name="server_address">Adresse du serveur</string>
|
||||
<string name="upload_errors">Erreurs de téléversement</string>
|
||||
<string name="app_check_for_updates_beta">Bêta</string>
|
||||
<string name="app_check_for_updates">Vérifier les mises à jour</string>
|
||||
<string name="app_check_for_updates_notice_title">Vérifier les mises à jour</string>
|
||||
<string name="smp_servers_configured">Serveurs SMP configurés</string>
|
||||
<string name="xftp_servers_configured">Serveurs XFTP configurés</string>
|
||||
<string name="app_check_for_updates_disabled">Désactivé</string>
|
||||
<string name="app_check_for_updates_installed_successfully_title">Installé avec succès</string>
|
||||
<string name="app_check_for_updates_button_install">Installer la mise à jour</string>
|
||||
<string name="app_check_for_updates_button_open">Ouvrir l\'emplacement du fichier</string>
|
||||
<string name="smp_servers_other">Autres serveurs SMP</string>
|
||||
<string name="xftp_servers_other">Autres serveurs XFTP</string>
|
||||
<string name="app_check_for_updates_installed_successfully_desc">Veuillez redémarrer l\'application.</string>
|
||||
<string name="app_check_for_updates_button_remind_later">Rappeler plus tard</string>
|
||||
<string name="subscription_percentage">Afficher le pourcentage</string>
|
||||
<string name="app_check_for_updates_button_skip">Sauter cette version</string>
|
||||
<string name="app_check_for_updates_stable">Stable</string>
|
||||
<string name="app_check_for_updates_notice_desc">Pour être informé des nouvelles versions, activez la vérification périodique des versions Stable ou Bêta.</string>
|
||||
<string name="app_check_for_updates_update_available">Mise à jour disponible : %s</string>
|
||||
<string name="app_check_for_updates_canceled">Téléchargement de la mise à jour annulé</string>
|
||||
<string name="appearance_font_size">Taille de police</string>
|
||||
<string name="appearance_zoom">Zoom</string>
|
||||
<string name="attempts_label">tentatives</string>
|
||||
<string name="servers_info_sessions_connected">Connecté</string>
|
||||
<string name="servers_info_sessions_connecting">Connexion</string>
|
||||
<string name="servers_info_details">Détails</string>
|
||||
<string name="servers_info_downloaded">Téléchargé</string>
|
||||
<string name="servers_info_modal_error_title">Erreur</string>
|
||||
<string name="servers_info_reconnect_servers_error">Erreur de reconnexion des serveurs</string>
|
||||
<string name="servers_info_sessions_errors">Erreurs</string>
|
||||
<string name="servers_info_files_tab">Fichiers</string>
|
||||
<string name="servers_info_subscriptions_section_header">Réception de message</string>
|
||||
<string name="servers_info_messages_received">Messages reçus</string>
|
||||
<string name="servers_info_messages_sent">Messages envoyés</string>
|
||||
<string name="servers_info_missing">Pas d\'information, essayez de recharger</string>
|
||||
<string name="servers_info_subscriptions_connections_pending">En attente</string>
|
||||
<string name="proxied">Routé via un proxy</string>
|
||||
<string name="servers_info_detailed_statistics_received_messages_header">Messages reçus</string>
|
||||
<string name="servers_info_detailed_statistics_received_total">Total reçu</string>
|
||||
<string name="servers_info_detailed_statistics_receive_errors">Erreurs de réception</string>
|
||||
<string name="reconnect">Reconnecter</string>
|
||||
<string name="servers_info_reconnect_all_servers_button">Reconnecter tous les serveurs</string>
|
||||
<string name="servers_info_reset_stats">Réinitialiser toutes les statistiques</string>
|
||||
<string name="servers_info_reset_stats_alert_title">Réinitialiser toutes les statistiques ?</string>
|
||||
<string name="sent_directly">Envoyé directement</string>
|
||||
<string name="servers_info_detailed_statistics_sent_messages_total">Total envoyé</string>
|
||||
<string name="sent_via_proxy">Envoyé via un proxy</string>
|
||||
<string name="servers_info">Infos serveurs</string>
|
||||
<string name="servers_info_target">Afficher les informations pour</string>
|
||||
<string name="servers_info_starting_from">À partir de %s.</string>
|
||||
<string name="servers_info_private_data_disclaimer">À partir de %s.
|
||||
\nToutes les données restent confinées dans votre appareil.</string>
|
||||
<string name="servers_info_statistics_section_header">Statistiques</string>
|
||||
<string name="servers_info_subscriptions_total">Total</string>
|
||||
<string name="xftp_server">Serveur XFTP</string>
|
||||
<string name="acknowledgement_errors">Erreurs d\'accusé de réception</string>
|
||||
<string name="chunks_uploaded">Chunks téléversés</string>
|
||||
<string name="connections">Connexions</string>
|
||||
<string name="created">Créé</string>
|
||||
<string name="deletion_errors">Erreurs de suppression</string>
|
||||
<string name="duplicates_label">doublons</string>
|
||||
<string name="expired_label">expiré</string>
|
||||
<string name="open_server_settings_button">Ouvrir les paramètres du serveur</string>
|
||||
<string name="other_label">autre</string>
|
||||
<string name="other_errors">autres erreurs</string>
|
||||
<string name="secured">Sécurisé</string>
|
||||
<string name="send_errors">Erreurs d\'envoi</string>
|
||||
<string name="size">Taille</string>
|
||||
<string name="subscribed">Inscrit</string>
|
||||
<string name="subscription_errors">Erreurs d\'inscription</string>
|
||||
<string name="subscription_results_ignored">Inscriptions ignorées</string>
|
||||
<string name="uploaded_files">Fichiers téléversés</string>
|
||||
</resources>
|
||||
@@ -149,7 +149,7 @@
|
||||
<string name="use_camera_button">Kamera</string>
|
||||
<string name="cannot_access_keychain">A Keystore-hoz nem sikerül hozzáférni az adatbázis jelszó mentése végett</string>
|
||||
<string name="callstatus_in_progress">hívás folyamatban</string>
|
||||
<string name="auto_accept_images">Fotók automatikus elfogadása</string>
|
||||
<string name="auto_accept_images">Képek automatikus elfogadása</string>
|
||||
<string name="allow_your_contacts_to_call">A hívások kezdeményezése engedélyezve van az ismerősei számára.</string>
|
||||
<string name="settings_section_title_icon">ALKALMAZÁS IKON</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">Kiszolgáló hozzáadása QR-kód beolvasásával.</string>
|
||||
@@ -293,7 +293,7 @@
|
||||
<string name="clear_verification">Hitelesítés törlése</string>
|
||||
<string name="group_member_status_creator">készítő</string>
|
||||
<string name="confirm_verb">Megerősítés</string>
|
||||
<string name="for_me_only">Törlés nálam</string>
|
||||
<string name="for_me_only">Csak nálam</string>
|
||||
<string name="delete_messages__question">%d üzenet törlése?</string>
|
||||
<string name="v5_1_custom_themes">Egyedi témák</string>
|
||||
<string name="group_member_status_accepted">kapcsolódás (elfogadva)</string>
|
||||
@@ -361,7 +361,7 @@
|
||||
<string name="database_encryption_will_be_updated">Az adatbázis titkosítás jelmondata megváltoztatásra és mentésre kerül a Keystore-ban.</string>
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Az adatbázis titkosításra kerül és a jelmondat eltárolásra a beállításokban.</string>
|
||||
<string name="smp_servers_delete_server">Kiszolgáló törlése</string>
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">Eszközhitelesítés kikapcsolva. SimpleX zárolás kikapcsolása.</string>
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">A készüléken nincs beállítva a képernyőzár. A SimpleX zár ki van kapcsolva.</string>
|
||||
<string name="no_call_on_lock_screen">Letiltás</string>
|
||||
<string name="receipts_groups_disable_for_all">Letiltás minden csoport számára</string>
|
||||
<string name="receipts_groups_enable_for_all">Engedélyezés minden csoport számára</string>
|
||||
@@ -372,7 +372,7 @@
|
||||
<string name="desktop_address">Számítógép címe</string>
|
||||
<string name="ttl_s">%dmp</string>
|
||||
<string name="delivery_receipts_title">Kézbesítési jelentések!</string>
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Eszközhitelesítés nem engedélyezett.A SimpleX zárolás bekapcsolható a Beállításokon keresztül, miután az eszköz hitelesítés engedélyezésre került.</string>
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">A készüléken nincs beállítva a képernyőzár. A SimpleX zár az „Adatvédelem és biztonság” menüben kapcsolható be, miután beállította a képernyőzárat az eszközén.</string>
|
||||
<string name="decryption_error">Titkosítás visszafejtési hiba</string>
|
||||
<string name="share_text_disappears_at">Eltűnik ekkor: %s</string>
|
||||
<string name="icon_descr_edited">szerkesztve</string>
|
||||
@@ -398,7 +398,7 @@
|
||||
<string name="disappearing_message">Eltűnő üzenet</string>
|
||||
<string name="dont_create_address">Ne hozzon létre címet</string>
|
||||
<string name="dont_show_again">Ne mutasd újra</string>
|
||||
<string name="auth_disable_simplex_lock">SimpleX zárolás kikapcsolása</string>
|
||||
<string name="auth_disable_simplex_lock">SimpleX zár kikapcsolása</string>
|
||||
<string name="status_e2e_encrypted">e2e titkosított</string>
|
||||
<string name="settings_section_title_device">ESZKÖZ</string>
|
||||
<string name="encrypted_video_call">e2e titkosított videóhívás</string>
|
||||
@@ -424,7 +424,7 @@
|
||||
<string name="feature_enabled_for_you">engedélyezve az ön számára</string>
|
||||
<string name="timed_messages">Eltűnő üzenetek</string>
|
||||
<string name="delete_group_menu_action">Törlés</string>
|
||||
<string name="delete_and_notify_contact">Törlés és ismerős értesítése</string>
|
||||
<string name="delete_and_notify_contact">Törlés, és az ismerős értesítése</string>
|
||||
<string name="send_receipts_disabled">letiltva</string>
|
||||
<string name="la_seconds">%d másodperc</string>
|
||||
<string name="delete_files_and_media_all">Minden fájl törlése</string>
|
||||
@@ -463,7 +463,7 @@
|
||||
<string name="database_passphrase_is_required">Adatbázis jelmondat szükséges a csevegés megnyitásához.</string>
|
||||
<string name="ttl_d">%dnap</string>
|
||||
<string name="receipts_contacts_enable_for_all">Engedélyezés mindenki számára</string>
|
||||
<string name="delivery_receipts_are_disabled">Kézbesítési jelentések kikapcsolva!</string>
|
||||
<string name="delivery_receipts_are_disabled">A kézbesítési jelentések le vannak tiltva!</string>
|
||||
<string name="expand_verb">Kibontás</string>
|
||||
<string name="error_sending_message">Hiba az üzenet küldésekor</string>
|
||||
<string name="la_enter_app_passcode">Jelkód megadása</string>
|
||||
@@ -613,7 +613,7 @@
|
||||
<string name="group_preferences">Csoport beállítások</string>
|
||||
<string name="error_with_info">Hiba: %s</string>
|
||||
<string name="v4_4_disappearing_messages">Eltűnő üzenetek</string>
|
||||
<string name="auth_enable_simplex_lock">SimpleX zárolás engedélyezése</string>
|
||||
<string name="auth_enable_simplex_lock">SimpleX zár bekapcsolása</string>
|
||||
<string name="error_synchronizing_connection">Hiba a kapcsolat szinkronizálása során</string>
|
||||
<string name="error_creating_address">Hiba a cím létrehozásakor</string>
|
||||
<string name="feature_enabled">engedélyezve</string>
|
||||
@@ -663,7 +663,7 @@
|
||||
<string name="icon_descr_instant_notifications">Azonnali értesítések</string>
|
||||
<string name="settings_section_title_incognito">Inkognitó mód</string>
|
||||
<string name="import_database_question">Csevegési adatbázis importálása?</string>
|
||||
<string name="service_notifications_disabled">Azonnali értesítések kikapcsolva!</string>
|
||||
<string name="service_notifications_disabled">Az azonnali értesítések le vannak tiltva!</string>
|
||||
<string name="service_notifications">Azonnali értesítések!</string>
|
||||
<string name="image_descr">Kép</string>
|
||||
<string name="files_are_prohibited_in_group">A fájlok- és a médiatartalom küldése le van tiltva ebben a csoportban.</string>
|
||||
@@ -678,7 +678,7 @@
|
||||
<string name="network_disable_socks_info">Megerősítés esetén az üzenetküldő kiszolgálók látni fogják az IP-címét és a szolgáltatóját – azt, hogy mely kiszolgálókhoz kapcsolódik.</string>
|
||||
<string name="image_will_be_received_when_contact_completes_uploading">A kép akkor érkezik meg, amikor a küldője befejezte annak feltöltését.</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 asztali számítógép: a megjelenített QR-kód beolvasása az alkalmazásból, a <b>QR kód beolvasásával</b>.]]></string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Kapott SimpleX Chat meghívó hivatkozását megnyithatja böngészőjében:</string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">A kapott SimpleX Chat meghívó hivatkozását megnyithatja böngészőjében:</string>
|
||||
<string name="if_you_enter_self_destruct_code">Ha az alkalmazás megnyitásakor megadja az önmegsemmisítő jelkódot:</string>
|
||||
<string name="found_desktop">Megtalált számítógép</string>
|
||||
<string name="desktop_devices">Számítógépek</string>
|
||||
@@ -726,7 +726,7 @@
|
||||
<string name="v4_6_reduced_battery_usage_descr">Hamarosan további fejlesztések érkeznek!</string>
|
||||
<string name="message_reactions_prohibited_in_this_chat">Az üzenetreakciók küldése le van tiltva ebben a csevegésben.</string>
|
||||
<string name="incorrect_code">Helytelen biztonsági kód!</string>
|
||||
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Ez akkor fordulhat elő, ha ön, vagy az ismerőse régi adatbázis biztonsági mentést használt.</string>
|
||||
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Ez akkor fordulhat elő, ha ön vagy az ismerőse régi adatbázis biztonsági mentést használt.</string>
|
||||
<string name="v5_3_new_desktop_app">Új asztali alkalmazás!</string>
|
||||
<string name="v4_6_group_moderation_descr">Most már az adminok is:
|
||||
\n- törölhetik a tagok üzeneteit.
|
||||
@@ -853,7 +853,7 @@
|
||||
<string name="leave_group_question">Csoport elhagyása?</string>
|
||||
<string name="chat_preferences_no">nem</string>
|
||||
<string name="v4_5_reduced_battery_usage_descr">Hamarosan további fejlesztések érkeznek!</string>
|
||||
<string name="feature_off">ki</string>
|
||||
<string name="feature_off">kikapcsolva</string>
|
||||
<string name="install_simplex_chat_for_terminal">SimpleX Chat telepítése a terminálhoz</string>
|
||||
<string name="self_destruct_new_display_name">Új megjelenített név:</string>
|
||||
<string name="new_passphrase">Új jelmondat…</string>
|
||||
@@ -1098,7 +1098,7 @@
|
||||
<string name="your_calls">Hívások</string>
|
||||
<string name="icon_descr_sent_msg_status_send_failed">nem sikerült elküldeni</string>
|
||||
<string name="theme_colors_section_title">KEZELŐFELÜLET SZÍNEI</string>
|
||||
<string name="network_options_revert">Visszaállít</string>
|
||||
<string name="network_options_revert">Visszaállítás</string>
|
||||
<string name="restore_database_alert_desc">Előző jelszó megadása az adatbázis biztonsági mentésének visszaállítása után. Ez a művelet nem vonható vissza.</string>
|
||||
<string name="color_secondary">Másodlagos</string>
|
||||
<string name="settings_section_title_socks">SOCKS PROXY</string>
|
||||
@@ -1192,7 +1192,7 @@
|
||||
<string name="reset_color">Színek alaphelyzetbe állítása</string>
|
||||
<string name="network_options_save">Mentés</string>
|
||||
<string name="switch_verb">Váltás</string>
|
||||
<string name="paste_the_link_you_received_to_connect_with_your_contact">Kapott hivatkozás beillesztése az ismerősökhöz történő kapcsolódáshoz…</string>
|
||||
<string name="paste_the_link_you_received_to_connect_with_your_contact">A kapott hivatkozás beillesztése az ismerősökhöz történő kapcsolódáshoz…</string>
|
||||
<string name="scan_code">Beolvasás</string>
|
||||
<string name="open_port_in_firewall_title">Port megnyitása a tűzfalon</string>
|
||||
<string name="callstate_starting">indítás…</string>
|
||||
@@ -1217,7 +1217,7 @@
|
||||
<string name="remove_passphrase">Eltávolítás</string>
|
||||
<string name="network_use_onion_hosts">Tor .onion kiszolgálók használata</string>
|
||||
<string name="reveal_verb">Felfedés</string>
|
||||
<string name="la_lock_mode">SimpleX zárolási mód</string>
|
||||
<string name="la_lock_mode">Zárolási mód</string>
|
||||
<string name="revoke_file__action">Fájl visszavonása</string>
|
||||
<string name="xftp_servers">XFTP kiszolgálók</string>
|
||||
<string name="prohibit_sending_files">A fájlok- és a médiatartalom küldése le van tiltva.</string>
|
||||
@@ -1256,21 +1256,21 @@
|
||||
<string name="v5_0_polish_interface">Lengyel kezelőfelület</string>
|
||||
<string name="smp_servers_use_server">Kiszolgáló használata</string>
|
||||
<string name="share_text_received_at">Fogadva ekkor: %s</string>
|
||||
<string name="la_notice_title_simplex_lock">SimpleX zárolás</string>
|
||||
<string name="la_notice_title_simplex_lock">SimpleX zár</string>
|
||||
<string name="save_and_notify_group_members">Mentés és csoporttagok értesítése</string>
|
||||
<string name="reset_verb">Alaphelyzetbe állítás</string>
|
||||
<string name="only_your_contact_can_add_message_reactions">Csak az ismerőse tud üzenetreakciókat küldeni.</string>
|
||||
<string name="voice_messages">Hangüzenetek</string>
|
||||
<string name="snd_group_event_user_left">elhagyta a csoportot</string>
|
||||
<string name="icon_descr_record_voice_message">Hangüzenet rögzítése</string>
|
||||
<string name="auth_simplex_lock_turned_on">SimpleX zárolás bekapcsolva</string>
|
||||
<string name="auth_simplex_lock_turned_on">SimpleX zár bekapcsolva</string>
|
||||
<string name="member_contact_send_direct_message">közvetlen üzenet küldése</string>
|
||||
<string name="scan_from_mobile">Beolvasás mobilról</string>
|
||||
<string name="verify_connections">Kapcsolatok ellenőrzése</string>
|
||||
<string name="share_message">Üzenet megosztása…</string>
|
||||
<string name="custom_time_unit_seconds">másodperc</string>
|
||||
<string name="lock_not_enabled">SimpleX zárolás nincs engedélyezve!</string>
|
||||
<string name="chat_lock">SimpleX zárolás</string>
|
||||
<string name="lock_not_enabled">A SimpleX zár nincs bekapcsolva!</string>
|
||||
<string name="chat_lock">SimpleX zár</string>
|
||||
<string name="your_settings">Beállítások</string>
|
||||
<string name="your_chat_database">Csevegési adatbázis</string>
|
||||
<string name="rcv_group_event_member_deleted">%1$s eltávolítva</string>
|
||||
@@ -1338,11 +1338,11 @@
|
||||
<string name="error_smp_test_server_auth">A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze jelszavát</string>
|
||||
<string name="you_will_join_group">Kapcsolódni fog a csoport összes tagjához.</string>
|
||||
<string name="error_smp_test_certificate">Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen</string>
|
||||
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Az adatavédelem érdekében kapcsolja be a SimpleX zárolás funkciót.
|
||||
\nA funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befejezésére.</string>
|
||||
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">A biztonsága érdekében kapcsolja be a SimpleX zár funkciót.
|
||||
\nA funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beállítására az eszközén.</string>
|
||||
<string name="video_will_be_received_when_contact_is_online">A videó akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később!</string>
|
||||
<string name="network_error_desc">Hálózati kapcsolat ellenőrzése a következővel: %1$s, és próbálja újra.</string>
|
||||
<string name="you_can_turn_on_lock">A SimpleX zárolás a Beállításokon keresztül kapcsolható be.</string>
|
||||
<string name="you_can_turn_on_lock">A SimpleX zár az „Adatvédelem és biztonság” menüben kapcsolható be.</string>
|
||||
<string name="app_was_crashed">Az alkalmazás összeomlott</string>
|
||||
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Ellenőrizze, hogy a megfelelő hivatkozást használta-e, vagy kérje meg ismerősét, hogy küldjön egy másikat.</string>
|
||||
<string name="image_decoding_exception_desc">A kép nem dekódolható. Próbálja meg egy másik képpel, vagy lépjen kapcsolatba a fejlesztőkkel.</string>
|
||||
@@ -1404,9 +1404,9 @@
|
||||
<string name="database_is_not_encrypted">A csevegési adatbázis nem titkosított - állítson be egy jelmondatot annak védelméhez.</string>
|
||||
<string name="network_disable_socks">Közvetlen internet kapcsolat használata?</string>
|
||||
<string name="you_will_still_receive_calls_and_ntfs">Továbbra is kap hívásokat és értesítéseket a némított profiloktól, ha azok aktívak.</string>
|
||||
<string name="group_main_profile_sent">A fő csevegési profilja megküldésre kerül a csoporttagok számára</string>
|
||||
<string name="group_main_profile_sent">A fő csevegési profilja elküldésre kerül a csoporttagok számára</string>
|
||||
<string name="you_can_enable_delivery_receipts_later_alert">Később engedélyezheti őket az alkalmazás Adatvédelem és biztonság menüpontban.</string>
|
||||
<string name="to_reveal_profile_enter_password">Rejtett profiljának felfedéséhez írja be a teljes jelszót a Csevegési profilok oldal keresőmezőjébe.</string>
|
||||
<string name="to_reveal_profile_enter_password">Rejtett profilja megjelenítéséhez írja be a teljes jelszavát a keresőmezőbe a Csevegési profilok menüben.</string>
|
||||
<string name="upgrade_and_open_chat">A csevegés frissítése és megnyitása</string>
|
||||
<string name="you_need_to_allow_to_send_voice">Hangüzeneteket küldéséhez engedélyeznie kell azok küldését az ismerősei számára.</string>
|
||||
<string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[Beállíthatja, hogy mely kiszolgáló(ko)n keresztül <b>fogadja</b> az üzeneteket, ismerősöket – a kiszolgálók, amelyeket az üzenetküldéshez használ.]]></string>
|
||||
@@ -1430,7 +1430,7 @@
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Kapcsolatba léphet <font color="#0088ff">a SimpleX Chat fejlesztőivel, ahol bármiről kérdezhet és értesülhet az újdonságokról</font>.]]></string>
|
||||
<string name="v4_2_auto_accept_contact_requests_desc">Opcionális üdvözlő üzenettel.</string>
|
||||
<string name="unknown_database_error_with_info">Ismeretlen adatbázis hiba: %s</string>
|
||||
<string name="you_can_hide_or_mute_user_profile">Elrejthet vagy némíthat egy felhasználói profilt - tartsa lenyomva a menühöz.</string>
|
||||
<string name="you_can_hide_or_mute_user_profile">Elrejtheti vagy lenémíthatja a felhasználó profiljait - koppintson (vagy asztali alkalmazásban kattintson) hosszan a profilra a felugró menühöz.</string>
|
||||
<string name="v5_3_simpler_incognito_mode_descr">Inkognító mód kapcsolódáskor.</string>
|
||||
<string name="update_onion_hosts_settings_question">Tor .onion kiszolgálók beállításainak frissítése?</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Megoszthat egy hivatkozást vagy QR-kódot - így bárki csatlakozhat a csoporthoz. Ha a csoport később törlésre kerül, akkor nem fogja elveszíteni annak tagjait.</string>
|
||||
@@ -1454,7 +1454,7 @@
|
||||
<string name="profile_is_only_shared_with_your_contacts">Profilja csak az ismerőseivel kerül megosztásra.</string>
|
||||
<string name="smp_servers_test_some_failed">Néhány kiszolgáló megbukott a teszten:</string>
|
||||
<string name="group_invitation_tap_to_join">Koppintson a csatlakozáshoz</string>
|
||||
<string name="delete_files_and_media_desc">Ez a művelet nem vonható vissza - az összes fogadott és küldött fájl a médiatartalommal együtt törlésre kerülnek. Az alacsony felbontású fotók viszont megmaradnak.</string>
|
||||
<string name="delete_files_and_media_desc">Ez a művelet nem vonható vissza - az összes fogadott és küldött fájl a médiatartalommal együtt törlésre kerülnek. Az alacsony felbontású képek viszont megmaradnak.</string>
|
||||
<string name="receipts_contacts_override_enabled">Kézbesítési jelentések engedélyezve vannak %d ismerősnél</string>
|
||||
<string name="sending_via">Küldés ezen keresztül:</string>
|
||||
<string name="v5_0_polish_interface_descr">Köszönet a felhasználóknak - hozzájárulás a Weblaten!</string>
|
||||
@@ -1589,7 +1589,7 @@
|
||||
<string name="group_member_status_unknown">ismeretlen státusz</string>
|
||||
<string name="profile_update_event_member_name_changed">%1$s megváltoztatta a nevét erre: %2$s</string>
|
||||
<string name="profile_update_event_removed_address">törölt kapcsolattartási cím</string>
|
||||
<string name="profile_update_event_removed_picture">törölt profilkép</string>
|
||||
<string name="profile_update_event_removed_picture">törölte a profilképét</string>
|
||||
<string name="profile_update_event_set_new_address">új kapcsolattartási cím beállítása</string>
|
||||
<string name="profile_update_event_set_new_picture">új profilképet állított be</string>
|
||||
<string name="profile_update_event_updated_profile">frissített profil</string>
|
||||
@@ -1700,7 +1700,7 @@
|
||||
<string name="migrate_from_device_migration_complete">Átköltöztetés befejezve</string>
|
||||
<string name="v5_6_app_data_migration_descr">Átköltöztetés egy másik eszközre QR-kód használatával.</string>
|
||||
<string name="migrate_to_device_migrating">Átköltöztetés</string>
|
||||
<string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Megjegyzés</b>: ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja a kapcsolataiból érkező üzenetek visszafejtését.]]></string>
|
||||
<string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Megjegyzés</b>: ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja az ismerőseitől érkező üzenetek visszafejtését.]]></string>
|
||||
<string name="migrate_to_device_try_again">Megpróbálhatja még egyszer.</string>
|
||||
<string name="invalid_file_link">Hibás hivatkozás</string>
|
||||
<string name="conn_event_enabled_pq">végpontok közötti kvantumrezisztens titkosítás</string>
|
||||
@@ -1884,7 +1884,7 @@
|
||||
<string name="servers_info_sessions_connecting">Kapcsolódás</string>
|
||||
<string name="servers_info_sessions_errors">Hibák</string>
|
||||
<string name="servers_info_subscriptions_connections_pending">Függőben</string>
|
||||
<string name="servers_info_private_data_disclaimer">Kezdve ettől: %s.
|
||||
<string name="servers_info_private_data_disclaimer">Ekkortól kezdve: %s.
|
||||
\nMinden adat biztonságban van a készülékén.</string>
|
||||
<string name="servers_info_messages_sent">Elküldött üzenetek</string>
|
||||
<string name="servers_info_proxied_servers_section_header">Proxyzott kiszolgálók</string>
|
||||
@@ -1909,7 +1909,7 @@
|
||||
<string name="servers_info_detailed_statistics_sent_messages_total">Összes elküldött</string>
|
||||
<string name="sent_via_proxy">Proxyn keresztül küldve</string>
|
||||
<string name="smp_server">SMP-kiszolgáló</string>
|
||||
<string name="servers_info_starting_from">Kezdve ettől: %s.</string>
|
||||
<string name="servers_info_starting_from">Ekkortól kezdve: %s.</string>
|
||||
<string name="servers_info_uploaded">Feltöltve</string>
|
||||
<string name="xftp_server">XFTP-kiszolgáló</string>
|
||||
<string name="proxied">Proxyzott</string>
|
||||
@@ -1930,13 +1930,13 @@
|
||||
<string name="acknowledgement_errors">Nyugtázott hibák</string>
|
||||
<string name="attempts_label">próbálkozások</string>
|
||||
<string name="chunks_deleted">Törölt fájltöredékek</string>
|
||||
<string name="all_users">Minden felhasználó</string>
|
||||
<string name="all_users">Összes profil</string>
|
||||
<string name="chunks_uploaded">Feltöltött fájltöredékek</string>
|
||||
<string name="completed">Elkészült</string>
|
||||
<string name="servers_info_connected_servers_section_header">Kapcsolódott kiszolgálók</string>
|
||||
<string name="xftp_servers_configured">Beállított XFTP-kiszolgálók</string>
|
||||
<string name="servers_info_sessions_connected">Kapcsolódva</string>
|
||||
<string name="current_user">Jelenlegi felhasználó</string>
|
||||
<string name="current_user">Jelenlegi profil</string>
|
||||
<string name="servers_info_details">Részletek</string>
|
||||
<string name="decryption_errors">visszafejtési hibák</string>
|
||||
<string name="deleted">Törölve</string>
|
||||
@@ -1959,12 +1959,12 @@
|
||||
<string name="servers_info_target">Információk megjelenítése ehhez:</string>
|
||||
<string name="network_error_broker_version_desc">A kiszolgáló verziója nem kompatibilis az alkalmazással: %1$s.</string>
|
||||
<string name="servers_info_proxied_servers_section_footer">Ön nem kapcsolódik ezekhez a kiszolgálókhoz. A privát útválasztás az üzenetek kézbesítésére szolgál.</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">Feliratkozott kapcsolatok</string>
|
||||
<string name="servers_info_subscriptions_section_header">Üzenet feliratkozások</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">Aktív kapcsolatok száma</string>
|
||||
<string name="servers_info_subscriptions_section_header">Üzenetjelentés</string>
|
||||
<string name="subscribed">Feliratkozva</string>
|
||||
<string name="subscription_errors">Feliratkozási hibák</string>
|
||||
<string name="subscription_results_ignored">Elutasított feliratkozások</string>
|
||||
<string name="subscription_percentage">Feliratkozási százalék</string>
|
||||
<string name="subscription_percentage">Százalék megjelenítése</string>
|
||||
<string name="app_check_for_updates_download_completed_title">Alkalmazásfrissítés letöltve</string>
|
||||
<string name="app_check_for_updates">Frissítések keresése</string>
|
||||
<string name="app_check_for_updates_notice_title">Frissítések keresése</string>
|
||||
|
||||
@@ -49,4 +49,53 @@
|
||||
<string name="about_simplex_chat">Tentang SimpleX Chat</string>
|
||||
<string name="accept">Terima</string>
|
||||
<string name="accept_call_on_lock_screen">Terima</string>
|
||||
<string name="cancel_verb">Batalkan</string>
|
||||
<string name="use_camera_button">Kamera</string>
|
||||
<string name="bold_text">tebal</string>
|
||||
<string name="callstatus_calling">menelepon…</string>
|
||||
<string name="audio_device_bluetooth">Bluetooth</string>
|
||||
<string name="icon_descr_call_ended">Panggilan ditutup</string>
|
||||
<string name="change_verb">Ubah</string>
|
||||
<string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Peringatan</b>: arsip akan dihapus.]]></string>
|
||||
<string name="create_group_button_to_create_new_group"><![CDATA[<b>Buat grup</b>: untuk membuat grup baru.]]></string>
|
||||
<string name="block_member_button">Blokir anggota</string>
|
||||
<string name="block_member_confirmation">Blokir</string>
|
||||
<string name="permissions_camera">Kamera</string>
|
||||
<string name="theme_black">Hitam</string>
|
||||
<string name="block_member_question">Blokir anggota?</string>
|
||||
<string name="switch_receiving_address">Ubah alamat penerima</string>
|
||||
<string name="app_check_for_updates_beta">Beta</string>
|
||||
<string name="block_for_all">Blokir untuk semua</string>
|
||||
<string name="block_for_all_question">Blokir anggota untuk semua?</string>
|
||||
<string name="call_already_ended">Panggilan telah ditutup!</string>
|
||||
<string name="network_type_cellular">Seluler</string>
|
||||
<string name="connect_via_invitation_link">Hubungkan melalui tautan satu kali?</string>
|
||||
<string name="connect_via_group_link">Gabung Grup?</string>
|
||||
<string name="connect_use_current_profile">Gunakan profil saat ini</string>
|
||||
<string name="connect_use_new_incognito_profile">Gunakan profil penyamaran baru</string>
|
||||
<string name="profile_will_be_sent_to_contact_sending_link">Profil Anda akan dikirim ke kontak yang menerima tautan ini.</string>
|
||||
<string name="you_will_join_group">Anda akan terhubung ke semua anggota grup.</string>
|
||||
<string name="connect_via_link_verb">Hubungkan</string>
|
||||
<string name="connect_via_link_incognito">Hubungkan penyamaran</string>
|
||||
<string name="opening_database">Membuka basis data…</string>
|
||||
<string name="thousand_abbreviation">k</string>
|
||||
<string name="connect_via_contact_link">Hubungkan melalui alamat kontak?</string>
|
||||
<string name="clear_verb">Bersihkan</string>
|
||||
<string name="clear_contacts_selection_button">Bersihkan</string>
|
||||
<string name="clear_chat_menu_action">Bersihkan</string>
|
||||
<string name="migrate_from_device_check_connection_and_try_again">Cek koneksi internetmu dan coba lagi</string>
|
||||
<string name="connect_button">Hubungkan</string>
|
||||
<string name="server_connected">terhubung</string>
|
||||
<string name="smp_server_test_connect">Hubungkan</string>
|
||||
<string name="notification_contact_connected">Terhubung</string>
|
||||
<string name="icon_descr_server_status_connected">Terhubung</string>
|
||||
<string name="rcv_group_event_member_connected">terhubung</string>
|
||||
<string name="connect_via_member_address_alert_title">Hubungkan langsung?</string>
|
||||
<string name="callstate_connected">tersambung</string>
|
||||
<string name="group_member_status_complete">selesai</string>
|
||||
<string name="group_member_status_connected">terhubung</string>
|
||||
<string name="connected_desktop">Komputer yang terhubung</string>
|
||||
<string name="servers_info_sessions_connected">Terhubung</string>
|
||||
<string name="completed">Selesai</string>
|
||||
<string name="connected_mobile">Ponsel yang terhubung</string>
|
||||
</resources>
|
||||
@@ -1874,7 +1874,7 @@
|
||||
<string name="cannot_share_message_alert_title">Impossibile inviare il messaggio</string>
|
||||
<string name="cannot_share_message_alert_text">Le preferenze della chat selezionata vietano questo messaggio.</string>
|
||||
<string name="connections">Connessioni</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">Connessioni sottoscritte</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">Connessioni attive</string>
|
||||
<string name="created">Creato</string>
|
||||
<string name="decryption_errors">errori di decifrazione</string>
|
||||
<string name="servers_info_detailed_statistics">Statistiche dettagliate</string>
|
||||
@@ -1883,7 +1883,7 @@
|
||||
<string name="servers_info_reconnect_server_error">Errore di riconnessione al server</string>
|
||||
<string name="servers_info_reconnect_servers_error">Errore di riconnessione ai server</string>
|
||||
<string name="expired_label">scaduto</string>
|
||||
<string name="servers_info_subscriptions_section_header">Iscrizioni ai messaggi</string>
|
||||
<string name="servers_info_subscriptions_section_header">Ricezione messaggi</string>
|
||||
<string name="other_label">altro</string>
|
||||
<string name="other_errors">altri errori</string>
|
||||
<string name="servers_info_subscriptions_connections_pending">In attesa</string>
|
||||
@@ -1933,12 +1933,12 @@
|
||||
<string name="member_inactive_desc">Il messaggio può essere consegnato più tardi se il membro diventa attivo.</string>
|
||||
<string name="smp_servers_configured">Server SMP configurati</string>
|
||||
<string name="smp_servers_other">Altri server SMP</string>
|
||||
<string name="subscription_percentage">Percentuale di iscrizione</string>
|
||||
<string name="subscription_percentage">Mostra percentuale</string>
|
||||
<string name="member_info_member_inactive">inattivo</string>
|
||||
<string name="appearance_zoom">Zoom</string>
|
||||
<string name="servers_info_sessions_connected">Connesso</string>
|
||||
<string name="servers_info_sessions_connecting">In connessione</string>
|
||||
<string name="current_user">Utente attuale</string>
|
||||
<string name="current_user">Profilo attuale</string>
|
||||
<string name="servers_info_details">Dettagli</string>
|
||||
<string name="servers_info_sessions_errors">Errori</string>
|
||||
<string name="servers_info_messages_received">Messaggi ricevuti</string>
|
||||
@@ -1948,7 +1948,7 @@
|
||||
<string name="servers_info_target">Informazioni di</string>
|
||||
<string name="servers_info_statistics_section_header">Statistiche</string>
|
||||
<string name="servers_info_transport_sessions_section_header">Sessioni di trasporto</string>
|
||||
<string name="all_users">Tutti gli utenti</string>
|
||||
<string name="all_users">Tutti i profili</string>
|
||||
<string name="attempts_label">tentativi</string>
|
||||
<string name="xftp_servers_configured">Server XFTP configurati</string>
|
||||
<string name="completed">Completato</string>
|
||||
@@ -1989,4 +1989,15 @@
|
||||
<string name="app_check_for_updates_download_completed_title">Aggiornamento dell\'app scaricato</string>
|
||||
<string name="app_check_for_updates_notice_title">Cerca aggiornamenti</string>
|
||||
<string name="app_check_for_updates_button_install">Installa aggiornamento</string>
|
||||
<string name="proxy_destination_error_failed_to_connect">Il server di inoltro %1$s non è riuscito a connettersi al server di destinazione %2$s. Riprova più tardi.</string>
|
||||
<string name="smp_proxy_error_broker_host">L\'indirizzo del server di inoltro è incompatibile con le impostazioni di rete: %1$s.</string>
|
||||
<string name="proxy_destination_error_broker_host">L\'indirizzo del server di destinazione di %1$s è incompatibile con le impostazioni del server di inoltro %2$s.</string>
|
||||
<string name="proxy_destination_error_broker_version">La versione del server di destinazione di %1$s è incompatibile con il server di inoltro %2$s.</string>
|
||||
<string name="smp_proxy_error_connecting">Errore di connessione al server di inoltro %1$s. Riprova più tardi.</string>
|
||||
<string name="smp_proxy_error_broker_version">La versione server di inoltro è incompatibile con le impostazioni di rete: %1$s.</string>
|
||||
<string name="privacy_media_blur_radius_off">Off</string>
|
||||
<string name="privacy_media_blur_radius">Sfocatura file multimediali</string>
|
||||
<string name="privacy_media_blur_radius_soft">Leggera</string>
|
||||
<string name="privacy_media_blur_radius_medium">Media</string>
|
||||
<string name="privacy_media_blur_radius_strong">Forte</string>
|
||||
</resources>
|
||||
@@ -863,7 +863,7 @@
|
||||
<string name="prohibit_direct_messages">Verbied het sturen van directe berichten naar leden.</string>
|
||||
<string name="prohibit_sending_voice">Verbieden het verzenden van spraak berichten.</string>
|
||||
<string name="v4_2_security_assessment_desc">De beveiliging van SimpleX Chat is gecontroleerd door Trail of Bits.</string>
|
||||
<string name="v4_2_auto_accept_contact_requests_desc">Met optioneel welkomst bericht.</string>
|
||||
<string name="v4_2_auto_accept_contact_requests_desc">Met optioneel welkom bericht.</string>
|
||||
<string name="v4_3_voice_messages">Spraak berichten</string>
|
||||
<string name="v4_3_irreversible_message_deletion_desc">Uw contacten kunnen volledige verwijdering van berichten toestaan.</string>
|
||||
<string name="you_have_to_enter_passphrase_every_time">U moet elke keer dat de app start het wachtwoord invoeren, deze wordt niet op het apparaat opgeslagen.</string>
|
||||
@@ -961,12 +961,12 @@
|
||||
<string name="v4_6_chinese_spanish_interface">Chinese en Spaanse interface</string>
|
||||
<string name="enter_password_to_show">Voer wachtwoord in bij zoeken</string>
|
||||
<string name="error_saving_user_password">Fout bij opslaan gebruikers wachtwoord</string>
|
||||
<string name="button_add_welcome_message">Welkomst bericht toevoegen</string>
|
||||
<string name="button_add_welcome_message">Welkom bericht toevoegen</string>
|
||||
<string name="dont_show_again">Niet meer weergeven</string>
|
||||
<string name="v4_6_group_moderation">Groep moderatie</string>
|
||||
<string name="error_updating_user_privacy">Fout bij updaten van gebruikers privacy</string>
|
||||
<string name="v4_6_reduced_battery_usage">Verder verminderd batterij verbruik</string>
|
||||
<string name="v4_6_group_welcome_message">Groep welkomst bericht</string>
|
||||
<string name="v4_6_group_welcome_message">Groep welkom bericht</string>
|
||||
<string name="v4_6_hidden_chat_profiles">Verborgen chat profielen</string>
|
||||
<string name="hide_profile">Profiel verbergen</string>
|
||||
<string name="user_hide">Verbergen</string>
|
||||
@@ -984,16 +984,16 @@
|
||||
<string name="smp_save_servers_question">Servers opslaan\?</string>
|
||||
<string name="save_profile_password">Bewaar profiel wachtwoord</string>
|
||||
<string name="v4_6_group_welcome_message_descr">Stel het getoonde bericht in voor nieuwe leden!</string>
|
||||
<string name="save_welcome_message_question">Welkomst bericht opslaan\?</string>
|
||||
<string name="save_welcome_message_question">Welkom bericht opslaan?</string>
|
||||
<string name="v4_6_audio_video_calls_descr">Ondersteuning voor bluetooth en andere verbeteringen.</string>
|
||||
<string name="tap_to_activate_profile">Tik om profiel te activeren.</string>
|
||||
<string name="v4_6_chinese_spanish_interface_descr">Dank aan de gebruikers – draag bij via Weblate!</string>
|
||||
<string name="you_can_hide_or_mute_user_profile">U kunt een gebruikers profiel verbergen of dempen - houd het vast voor het menu.</string>
|
||||
<string name="user_unhide">zichtbaar maken</string>
|
||||
<string name="user_unmute">Dempen opheffen</string>
|
||||
<string name="group_welcome_title">Welkomst bericht</string>
|
||||
<string name="group_welcome_title">Welkom bericht</string>
|
||||
<string name="to_reveal_profile_enter_password">Om uw verborgen profiel te onthullen, voert u een volledig wachtwoord in een zoekveld in op de pagina Uw chat profielen.</string>
|
||||
<string name="button_welcome_message">Welkomst bericht</string>
|
||||
<string name="button_welcome_message">Welkom bericht</string>
|
||||
<string name="you_will_still_receive_calls_and_ntfs">U ontvangt nog steeds oproepen en meldingen van gedempte profielen wanneer deze actief zijn.</string>
|
||||
<string name="database_downgrade">Database downgraden</string>
|
||||
<string name="invalid_migration_confirmation">Ongeldige migratie bevestiging</string>
|
||||
@@ -1135,13 +1135,13 @@
|
||||
<string name="email_invite_subject">Laten we praten in SimpleX Chat</string>
|
||||
<string name="save_auto_accept_settings">Sla instellingen voor automatisch accepteren op</string>
|
||||
<string name="save_settings_question">Instellingen opslaan\?</string>
|
||||
<string name="enter_welcome_message_optional">Voer welkomst bericht in... (optioneel)</string>
|
||||
<string name="enter_welcome_message_optional">Voer welkom bericht in... (optioneel)</string>
|
||||
<string name="dont_create_address">Maak geen adres aan</string>
|
||||
<string name="email_invite_body">Hoi!
|
||||
\nMaak verbinding met mij via SimpleX Chat: %s</string>
|
||||
<string name="you_can_create_it_later">U kan het later maken</string>
|
||||
<string name="share_address">Adres delen</string>
|
||||
<string name="enter_welcome_message">Welkomst bericht invoeren…</string>
|
||||
<string name="enter_welcome_message">Welkom bericht invoeren…</string>
|
||||
<string name="import_theme">Thema importeren</string>
|
||||
<string name="theme_simplex">SimpleX</string>
|
||||
<string name="color_primary_variant">Extra accent</string>
|
||||
@@ -1181,18 +1181,18 @@
|
||||
<string name="if_you_enter_self_destruct_code">Als u uw zelfvernietigings wachtwoord invoert tijdens het openen van de app:</string>
|
||||
<string name="if_you_enter_passcode_data_removed">Als u deze toegangscode invoert bij het openen van de app, worden alle app-gegevens onomkeerbaar verwijderd!</string>
|
||||
<string name="set_passcode">Toegangscode instellen</string>
|
||||
<string name="prohibit_message_reactions">Berichtreacties verbieden.</string>
|
||||
<string name="only_you_can_add_message_reactions">Alleen jij kunt berichtreacties toevoegen.</string>
|
||||
<string name="prohibit_message_reactions">Bericht reacties verbieden.</string>
|
||||
<string name="only_you_can_add_message_reactions">Alleen jij kunt bericht reacties toevoegen.</string>
|
||||
<string name="message_reactions_are_prohibited">Reacties op berichten zijn verboden in deze groep.</string>
|
||||
<string name="prohibit_message_reactions_group">Berichten reacties verbieden.</string>
|
||||
<string name="allow_message_reactions_only_if">Sta berichtreacties alleen toe als uw contact dit toestaat.</string>
|
||||
<string name="allow_your_contacts_adding_message_reactions">Sta uw contactpersonen toe om berichtreacties toe te voegen.</string>
|
||||
<string name="allow_message_reactions">Sta berichtreacties toe.</string>
|
||||
<string name="group_members_can_add_message_reactions">Groepsleden kunnen berichtreacties toevoegen.</string>
|
||||
<string name="both_you_and_your_contact_can_add_message_reactions">Zowel u als uw contact kunnen berichtreacties toevoegen.</string>
|
||||
<string name="allow_message_reactions_only_if">Sta bericht reacties alleen toe als uw contact dit toestaat.</string>
|
||||
<string name="allow_your_contacts_adding_message_reactions">Sta uw contactpersonen toe om bericht reacties toe te voegen.</string>
|
||||
<string name="allow_message_reactions">Sta bericht reacties toe.</string>
|
||||
<string name="group_members_can_add_message_reactions">Groepsleden kunnen bericht reacties toevoegen.</string>
|
||||
<string name="both_you_and_your_contact_can_add_message_reactions">Zowel u als uw contact kunnen bericht reacties toevoegen.</string>
|
||||
<string name="message_reactions">Reacties op berichten</string>
|
||||
<string name="message_reactions_prohibited_in_this_chat">Reacties op berichten zijn verboden in deze chat.</string>
|
||||
<string name="only_your_contact_can_add_message_reactions">Alleen uw contact kan berichtreacties toevoegen.</string>
|
||||
<string name="only_your_contact_can_add_message_reactions">Alleen uw contact kan bericht reacties toevoegen.</string>
|
||||
<string name="custom_time_unit_days">dagen</string>
|
||||
<string name="custom_time_unit_hours">uren</string>
|
||||
<string name="custom_time_unit_minutes">minuten</string>
|
||||
@@ -1628,7 +1628,7 @@
|
||||
<string name="snd_group_event_member_blocked">je hebt %s geblokkeerd</string>
|
||||
<string name="snd_group_event_member_unblocked">je hebt %s gedeblokkeerd</string>
|
||||
<string name="message_too_large">Bericht te groot</string>
|
||||
<string name="welcome_message_is_too_long">Welkomstbericht is te lang</string>
|
||||
<string name="welcome_message_is_too_long">Welkom bericht is te lang</string>
|
||||
<string name="database_migration_in_progress">De databasemigratie wordt uitgevoerd.
|
||||
\nDit kan enkele minuten duren.</string>
|
||||
<string name="call_service_notification_audio_call">Audio oproep</string>
|
||||
@@ -1834,7 +1834,7 @@
|
||||
<string name="v5_8_private_routing_descr">Bescherm uw IP-adres tegen de berichtenrelais die door uw contacten zijn gekozen.
|
||||
\nSchakel dit in in *Netwerk en servers*-instellingen.</string>
|
||||
<string name="wallpaper_scale_repeat">Herhalen</string>
|
||||
<string name="settings_section_title_chat_colors">Chatkleuren</string>
|
||||
<string name="settings_section_title_chat_colors">Chat kleuren</string>
|
||||
<string name="settings_section_title_user_theme">Profiel thema</string>
|
||||
<string name="settings_section_title_chat_theme">Chat thema</string>
|
||||
<string name="color_primary_variant2">Extra accent 2</string>
|
||||
@@ -1870,7 +1870,7 @@
|
||||
<string name="share_text_file_status">Bestandsstatus: %s</string>
|
||||
<string name="info_row_message_status">Berichtstatus</string>
|
||||
<string name="cannot_share_message_alert_title">Kan bericht niet verzenden</string>
|
||||
<string name="cannot_share_message_alert_text">Geselecteerde chatvoorkeuren verbieden dit bericht.</string>
|
||||
<string name="cannot_share_message_alert_text">Geselecteerde chat voorkeuren verbieden dit bericht.</string>
|
||||
<string name="private_routing_error">Fout in privéroutering</string>
|
||||
<string name="network_error_broker_version_desc">Serverversie is niet compatibel met uw app: %1$s.</string>
|
||||
<string name="message_forwarded_title">Bericht doorgestuurd</string>
|
||||
@@ -1878,17 +1878,17 @@
|
||||
<string name="xftp_servers_other">Overige XFTP servers</string>
|
||||
<string name="scan_paste_link">Link scannen/plakken</string>
|
||||
<string name="appearance_zoom">Zoom</string>
|
||||
<string name="current_user">Huidige gebruiker</string>
|
||||
<string name="current_user">Huidig profiel</string>
|
||||
<string name="servers_info_files_tab">Bestanden</string>
|
||||
<string name="servers_info">Server informatie</string>
|
||||
<string name="servers_info_target">Informatie weergeven voor</string>
|
||||
<string name="servers_info_sessions_errors">Fouten</string>
|
||||
<string name="servers_info_statistics_section_header">Statistieken</string>
|
||||
<string name="servers_info_transport_sessions_section_header">Transportsessies</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">Verbindingen geabonneerd</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">Actieve verbindingen</string>
|
||||
<string name="servers_info_details">Details</string>
|
||||
<string name="servers_info_messages_received">Berichten ontvangen</string>
|
||||
<string name="servers_info_subscriptions_section_header">Berichten abonnementen</string>
|
||||
<string name="servers_info_subscriptions_section_header">Bericht ontvangst</string>
|
||||
<string name="servers_info_private_data_disclaimer">Beginnend vanaf %s.
|
||||
\nAlle gegevens zijn privé op uw apparaat.</string>
|
||||
<string name="servers_info_connected_servers_section_header">Verbonden servers</string>
|
||||
@@ -1933,13 +1933,13 @@
|
||||
<string name="downloaded_files">Gedownloade bestanden</string>
|
||||
<string name="secured">Beveiligd</string>
|
||||
<string name="size">Maat</string>
|
||||
<string name="subscribed">Geabonneerd</string>
|
||||
<string name="subscribed">Ingeschreven</string>
|
||||
<string name="uploaded_files">Geüploade bestanden</string>
|
||||
<string name="upload_errors">Upload fouten</string>
|
||||
<string name="download_errors">Downloadfouten</string>
|
||||
<string name="open_server_settings_button">Server instellingen openen</string>
|
||||
<string name="server_address">Server adres</string>
|
||||
<string name="all_users">Alle gebruikers</string>
|
||||
<string name="all_users">Alle profielen</string>
|
||||
<string name="attempts_label">pogingen</string>
|
||||
<string name="chunks_deleted">Stukken verwijderd</string>
|
||||
<string name="completed">voltooid</string>
|
||||
@@ -1963,4 +1963,28 @@
|
||||
<string name="network_error_broker_host_desc">Het serveradres is niet compatibel met de netwerkinstellingen: %1$s.</string>
|
||||
<string name="servers_info_reset_stats_alert_message">Serverstatistieken worden gereset - dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="servers_info_starting_from">Beginnend vanaf %s.</string>
|
||||
<string name="smp_servers_configured">Geconfigureerde SMP-servers</string>
|
||||
<string name="xftp_servers_configured">Geconfigureerde XFTP-servers</string>
|
||||
<string name="subscription_percentage">Percentage weergeven</string>
|
||||
<string name="app_check_for_updates_disabled">Uitgeschakeld</string>
|
||||
<string name="app_check_for_updates_download_started">App update downloaden. Sluit de app niet</string>
|
||||
<string name="app_check_for_updates_button_download">%s downloaden (%s)</string>
|
||||
<string name="app_check_for_updates_installed_successfully_title">Succesvol geïnstalleerd</string>
|
||||
<string name="app_check_for_updates_button_install">Installeer update</string>
|
||||
<string name="app_check_for_updates_button_open">Open de bestandslocatie</string>
|
||||
<string name="app_check_for_updates_installed_successfully_desc">Herstart de app.</string>
|
||||
<string name="app_check_for_updates_button_remind_later">Herinner later</string>
|
||||
<string name="app_check_for_updates_button_skip">Sla deze versie over</string>
|
||||
<string name="member_info_member_disabled">uitgeschakeld</string>
|
||||
<string name="app_check_for_updates_download_completed_title">App update is gedownload</string>
|
||||
<string name="app_check_for_updates_beta">Beta</string>
|
||||
<string name="app_check_for_updates">Controleer op updates</string>
|
||||
<string name="app_check_for_updates_notice_title">Controleer op updates</string>
|
||||
<string name="app_check_for_updates_notice_disable">Uitschakelen</string>
|
||||
<string name="subscription_errors">Inschrijving fouten</string>
|
||||
<string name="subscription_results_ignored">Inschrijvingen genegeerd</string>
|
||||
<string name="app_check_for_updates_stable">Stabiel</string>
|
||||
<string name="app_check_for_updates_update_available">Update beschikbaar: %s</string>
|
||||
<string name="app_check_for_updates_canceled">Downloaden van update geannuleerd</string>
|
||||
<string name="app_check_for_updates_notice_desc">Als u op de hoogte wilt worden gehouden van de nieuwe releases, schakelt u periodieke controle op stabiele of bètaversies in.</string>
|
||||
</resources>
|
||||
@@ -1873,4 +1873,126 @@
|
||||
<string name="file_error_auth">Zły klucz lub nieznany adres fragmentu pliku - najprawdopodobniej plik został usunięty.</string>
|
||||
<string name="cannot_share_message_alert_title">Nie można wysłać wiadomości</string>
|
||||
<string name="cannot_share_message_alert_text">Wybrane preferencje czatu zabraniają tej wiadomości.</string>
|
||||
<string name="smp_proxy_error_connecting">Błąd połączenia z serwerem przekierowania %1$s. Spróbuj ponownie później.</string>
|
||||
<string name="proxy_destination_error_broker_host">Adres serwera docelowego %1$s jest niekompatybilny z ustawieniami serwera przekazującego %2$s.</string>
|
||||
<string name="proxy_destination_error_failed_to_connect">Serwer przekazujący %1$s nie mógł połączyć się z serwerem docelowym %2$s. Spróbuj ponownie później.</string>
|
||||
<string name="smp_proxy_error_broker_version">Wersja serwera przekierowującego jest niekompatybilna z ustawieniami sieciowymi: %1$s.</string>
|
||||
<string name="proxy_destination_error_broker_version">Wersja serwera docelowego %1$s jest niekompatybilna z serwerem przekierowującym %2$s.</string>
|
||||
<string name="member_inactive_title">Członek nieaktywny</string>
|
||||
<string name="message_forwarded_title">Wiadomość przekazana</string>
|
||||
<string name="member_inactive_desc">Wiadomość może zostać dostarczona później jeśli członek stanie się aktywny.</string>
|
||||
<string name="app_check_for_updates_beta">Beta</string>
|
||||
<string name="app_check_for_updates">Sprawdź aktualizacje</string>
|
||||
<string name="app_check_for_updates_disabled">Wyłączony</string>
|
||||
<string name="app_check_for_updates_button_download">Pobierz %s (%s)</string>
|
||||
<string name="app_check_for_updates_download_completed_title">Aktualizacja aplikacji jest pobrana</string>
|
||||
<string name="app_check_for_updates_notice_title">Sprawdź aktualizacje</string>
|
||||
<string name="app_check_for_updates_download_started">Pobieranie aktualizacji aplikacji, nie zamykaj aplikacji</string>
|
||||
<string name="app_check_for_updates_installed_successfully_title">Zainstalowano pomyślnie</string>
|
||||
<string name="app_check_for_updates_button_install">Zainstaluj aktualizacje</string>
|
||||
<string name="app_check_for_updates_notice_disable">Wyłącz</string>
|
||||
<string name="member_info_member_disabled">wyłączony</string>
|
||||
<string name="member_info_member_inactive">nieaktywny</string>
|
||||
<string name="appearance_font_size">Rozmiar czcionki</string>
|
||||
<string name="servers_info_sessions_connected">Połączony</string>
|
||||
<string name="current_user">Bieżący profil</string>
|
||||
<string name="servers_info_messages_received">Otrzymane wiadomości</string>
|
||||
<string name="servers_info_subscriptions_section_header">Odebranie wiadomości</string>
|
||||
<string name="servers_info_reset_stats_alert_error_title">Błąd resetowania statystyk</string>
|
||||
<string name="duplicates_label">duplikaty</string>
|
||||
<string name="completed">Zakończono</string>
|
||||
<string name="connections">Połączenia</string>
|
||||
<string name="created">Utworzono</string>
|
||||
<string name="deletion_errors">Błędy usuwania</string>
|
||||
<string name="chunks_downloaded">Fragmenty pobrane</string>
|
||||
<string name="chunks_uploaded">Fragmenty przesłane</string>
|
||||
<string name="downloaded_files">Pobrane pliki</string>
|
||||
<string name="smp_servers_configured">Skonfigurowane serwery SMP</string>
|
||||
<string name="acknowledged">Potwierdzono</string>
|
||||
<string name="acknowledgement_errors">Błędy potwierdzenia</string>
|
||||
<string name="deleted">Usunięto</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">Aktywne połączenia</string>
|
||||
<string name="all_users">Wszystkie profile</string>
|
||||
<string name="xftp_servers_configured">Skonfigurowane serwery XFTP</string>
|
||||
<string name="decryption_errors">błąd odszyfrowywania</string>
|
||||
<string name="servers_info_detailed_statistics">Szczegółowe statystyki</string>
|
||||
<string name="servers_info_details">Szczegóły</string>
|
||||
<string name="download_errors">Błędy pobierania</string>
|
||||
<string name="smp_proxy_error_broker_host">Adres serwera przekierowującego jest niekompatybilny z ustawieniami sieciowymi: %1$s.</string>
|
||||
<string name="servers_info_files_tab">Pliki</string>
|
||||
<string name="servers_info_sessions_connecting">Łączenie</string>
|
||||
<string name="servers_info_sessions_errors">Błędy</string>
|
||||
<string name="servers_info_connected_servers_section_header">Połączone serwery</string>
|
||||
<string name="servers_info_modal_error_title">Błąd</string>
|
||||
<string name="servers_info_reconnect_server_error">Błąd ponownego łączenia z serwerem</string>
|
||||
<string name="servers_info_reconnect_servers_error">Błąd ponownego łączenia serwerów</string>
|
||||
<string name="servers_info_downloaded">Pobrane</string>
|
||||
<string name="attempts_label">próby</string>
|
||||
<string name="expired_label">wygasły</string>
|
||||
<string name="chunks_deleted">Fragmenty usunięte</string>
|
||||
<string name="message_forwarded_desc">Brak bezpośredniego połączenia, wiadomość została przekazana przez administratora.</string>
|
||||
<string name="smp_servers_other">Inne serwery SMP</string>
|
||||
<string name="xftp_servers_other">Inne serwery XFTP</string>
|
||||
<string name="subscription_percentage">Pokaż procent</string>
|
||||
<string name="app_check_for_updates_stable">Stabilny</string>
|
||||
<string name="app_check_for_updates_update_available">Aktualizacja dostępna: %s</string>
|
||||
<string name="app_check_for_updates_button_open">Otwórz lokalizację pliku</string>
|
||||
<string name="app_check_for_updates_installed_successfully_desc">Proszę zrestartować aplikację.</string>
|
||||
<string name="app_check_for_updates_button_remind_later">Przypomnij później</string>
|
||||
<string name="app_check_for_updates_button_skip">Pomiń tę wersję</string>
|
||||
<string name="app_check_for_updates_canceled">Pobieranie aktualizacji anulowane</string>
|
||||
<string name="app_check_for_updates_notice_desc">Aby otrzymywać powiadomienia o nowych wersjach, włącz okresowe sprawdzanie wersji Stabilnych lub Beta.</string>
|
||||
<string name="appearance_zoom">Przybliż</string>
|
||||
<string name="servers_info_missing">Brak informacji, spróbuj przeładować</string>
|
||||
<string name="servers_info">Informacje o serwerach</string>
|
||||
<string name="servers_info_target">Wyświetlanie informacji dla</string>
|
||||
<string name="servers_info_statistics_section_header">Statystyki</string>
|
||||
<string name="servers_info_transport_sessions_section_header">Sesje transportowe</string>
|
||||
<string name="servers_info_private_data_disclaimer">Zaczynanie od %s.
|
||||
\nWszystkie dane są prywatne na Twoim urządzeniu.</string>
|
||||
<string name="servers_info_reconnect_all_servers_button">Połącz ponownie wszystkie serwery</string>
|
||||
<string name="servers_info_reconnect_server_title">Połączyć ponownie serwer?</string>
|
||||
<string name="servers_info_proxied_servers_section_footer">Nie jesteś połączony z tymi serwerami. Prywatne trasowanie jest używane do dostarczania do nich wiadomości.</string>
|
||||
<string name="servers_info_detailed_statistics_received_messages_header">Otrzymane wiadomości</string>
|
||||
<string name="servers_info_reset_stats_alert_confirm">Resetuj</string>
|
||||
<string name="servers_info_reset_stats">Resetuj wszystkie statystyki</string>
|
||||
<string name="servers_info_detailed_statistics_sent_messages_header">Wysłane wiadomości</string>
|
||||
<string name="servers_info_reset_stats_alert_message">Statystyki serwerów zostaną zresetowane - nie można tego cofnąć!</string>
|
||||
<string name="servers_info_uploaded">Przesłane</string>
|
||||
<string name="other_label">inne</string>
|
||||
<string name="proxied">Trasowane przez proxy</string>
|
||||
<string name="servers_info_detailed_statistics_received_total">Otrzymano łącznie</string>
|
||||
<string name="other_errors">inne błędy</string>
|
||||
<string name="secured">Zabezpieczone</string>
|
||||
<string name="subscribed">Zasubskrybowano</string>
|
||||
<string name="uploaded_files">Przesłane pliki</string>
|
||||
<string name="open_server_settings_button">Otwórz ustawienia serwera</string>
|
||||
<string name="server_address">Adres serwera</string>
|
||||
<string name="servers_info_messages_sent">Wysłane wiadomości</string>
|
||||
<string name="servers_info_reconnect_servers_message">Ponownie połącz ze wszystkimi połączonymi serwerami w celu wymuszenia dostarczenia wiadomości. Wykorzystuje to dodatkowy ruch.</string>
|
||||
<string name="servers_info_reset_stats_alert_title">Zresetować wszystkie statystyki?</string>
|
||||
<string name="subscription_errors">Błędy subskrypcji</string>
|
||||
<string name="subscription_results_ignored">Subskrypcje zignorowane</string>
|
||||
<string name="servers_info_detailed_statistics_sent_messages_total">Wysłano łącznie</string>
|
||||
<string name="network_error_broker_host_desc">Adres serwera jest niekompatybilny z ustawieniami sieci: %1$s.</string>
|
||||
<string name="please_try_later">Proszę spróbować później.</string>
|
||||
<string name="private_routing_error">Błąd prywatnego trasowania</string>
|
||||
<string name="network_error_broker_version_desc">Wersja serwera jest niekompatybilna z aplikacją: %1$s.</string>
|
||||
<string name="scan_paste_link">Skanuj / Wklej link</string>
|
||||
<string name="servers_info_subscriptions_total">Łącznie</string>
|
||||
<string name="servers_info_subscriptions_connections_pending">Oczekujące</string>
|
||||
<string name="servers_info_previously_connected_servers_section_header">Wcześniej połączone serwery</string>
|
||||
<string name="servers_info_proxied_servers_section_header">Serwery trasowane przez proxy</string>
|
||||
<string name="servers_info_reconnect_servers_title">Połączyć ponownie serwery?</string>
|
||||
<string name="servers_info_reconnect_server_message">Ponownie połącz z serwerem w celu wymuszenia dostarczenia wiadomości. Wykorzystuje to dodatkowy ruch.</string>
|
||||
<string name="sent_via_proxy">Wysłano przez proxy</string>
|
||||
<string name="xftp_server">Serwer XFTP</string>
|
||||
<string name="smp_server">Serwer SMP</string>
|
||||
<string name="servers_info_detailed_statistics_receive_errors">Błędy otrzymania</string>
|
||||
<string name="reconnect">Połącz ponownie</string>
|
||||
<string name="sent_directly">Wysłano bezpośrednio</string>
|
||||
<string name="servers_info_starting_from">Zaczynanie od %s.</string>
|
||||
<string name="send_errors">Wyślij błędy</string>
|
||||
<string name="size">Rozmiar</string>
|
||||
<string name="upload_errors">Błędy przesłania</string>
|
||||
</resources>
|
||||
@@ -1658,4 +1658,10 @@
|
||||
<string name="allow_to_send_simplex_links">Permitir o envio de links do SimpleX.</string>
|
||||
<string name="feature_roles_all_members">Todos os membros</string>
|
||||
<string name="wallpaper_advanced_settings">Configurações avançadas</string>
|
||||
<string name="app_check_for_updates_notice_title">Verificar atualizações</string>
|
||||
<string name="completed">Completado</string>
|
||||
<string name="smp_servers_configured">Servidores SMP configurados</string>
|
||||
<string name="xftp_servers_configured">Servidores XFTP configurados</string>
|
||||
<string name="migrate_from_device_check_connection_and_try_again">Verifique sua conexão de internet e tente novamente</string>
|
||||
<string name="app_check_for_updates">Verificar atualizações</string>
|
||||
</resources>
|
||||
@@ -482,4 +482,64 @@
|
||||
<string name="desktop_device">Máy tính</string>
|
||||
<string name="desktop_address">Địa chỉ máy tính</string>
|
||||
<string name="remote_ctrl_error_bad_version">Máy tính có một phiên bản không được hỗ trợ. Vui lòng đảm bảo rằng bạn sử dụng cùng một phiên bản ở cả hai thiết bị.</string>
|
||||
<string name="acknowledged">Đã xác nhận</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">Các kết nối đang hoạt động</string>
|
||||
<string name="all_users">Tất cả hồ sơ</string>
|
||||
<string name="app_check_for_updates_beta">Beta</string>
|
||||
<string name="app_check_for_updates">Kiểm tra cập nhật</string>
|
||||
<string name="servers_info_sessions_connecting">Đang kết nối</string>
|
||||
<string name="smp_servers_configured">Các máy chủ SMP đã được cấu hình</string>
|
||||
<string name="xftp_servers_configured">Các máy chủ XFTP đã được cấu hình</string>
|
||||
<string name="app_check_for_updates_download_completed_title">Bản cập nhật ứng dụng đã được tải xuống</string>
|
||||
<string name="app_check_for_updates_notice_title">Kiểm tra cập nhật</string>
|
||||
<string name="servers_info_sessions_connected">Đã kết nối</string>
|
||||
<string name="attempts_label">thử</string>
|
||||
<string name="servers_info_connected_servers_section_header">Các máy chủ đã kết nối</string>
|
||||
<string name="acknowledgement_errors">Lỗi xác nhận</string>
|
||||
<string name="completed">Đã hoàn thành</string>
|
||||
<string name="chunks_deleted">Các khúc đã bị xóa</string>
|
||||
<string name="chunks_downloaded">Các khúc đã được tải xuống</string>
|
||||
<string name="chunks_uploaded">Các khúc đã được tải lên</string>
|
||||
<string name="current_user">Hồ sơ hiện tại</string>
|
||||
<string name="decryption_errors">lỗi giải mã</string>
|
||||
<string name="servers_info_detailed_statistics">Thống kê chi tiết</string>
|
||||
<string name="servers_info_details">Chi tiết</string>
|
||||
<string name="connections">Các kết nối</string>
|
||||
<string name="created">Đã tạo</string>
|
||||
<string name="deleted">Đã xóa</string>
|
||||
<string name="deletion_errors">Lỗi xóa</string>
|
||||
<string name="rcv_group_events_count">%d sự kiện nhóm</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">Tin nhắn trực tiếp giữa các thành viên bị cấm trong nhóm này.</string>
|
||||
<string name="total_files_count_and_size">%d tệp với tổng kích thước là %s</string>
|
||||
<string name="mtr_error_different">phần di dời khác nhau trong ứng dụng/cơ sở dữ liệu: %s / %s</string>
|
||||
<string name="direct_messages">Tin nhắn trực tiếp</string>
|
||||
<string name="ttl_h">%dh</string>
|
||||
<string name="ttl_hour">%d giờ</string>
|
||||
<string name="ttl_hours">%d giờ</string>
|
||||
<string name="v4_5_multiple_chat_profiles_descr">Tên, hình đại diện và cách ly truyền tải khác nhau.</string>
|
||||
<string name="devices">Thiết bị</string>
|
||||
<string name="conn_level_desc_direct">trực tiếp</string>
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Xác thực thiết bị không được bật. Bạn có thể bật SimpleX Lock thông qua phần Cài đặt, sau khi bạn bật xác thực thiết bị.</string>
|
||||
<string name="receipts_contacts_title_disable">Tắt chỉ báo đã nhận?</string>
|
||||
<string name="timed_messages">Tin nhắn tự xóa</string>
|
||||
<string name="disappearing_message">Tin nhắn tự xóa</string>
|
||||
<string name="smp_server_test_disconnect">Ngắt kết nối</string>
|
||||
<string name="receipts_contacts_disable_keep_overrides">Tắt (giữ thông tin ghi đè)</string>
|
||||
<string name="share_text_disappears_at">Biến mất vào lúc: %s</string>
|
||||
<string name="v4_4_disappearing_messages">Tin nhắn tự xóa</string>
|
||||
<string name="receipts_contacts_disable_for_all">Tắt cho tất cả</string>
|
||||
<string name="receipts_groups_disable_for_all">Tắt cho tất cả các nhóm</string>
|
||||
<string name="info_row_disappears_at">Biến mất vào lúc</string>
|
||||
<string name="disconnect_remote_host">Ngắt kết nối</string>
|
||||
<string name="disable_notifications_button">Tắt thông báo</string>
|
||||
<string name="auth_disable_simplex_lock">Tắt SimpleX Lock</string>
|
||||
<string name="receipts_groups_disable_keep_overrides">Tắt (giữ thông tin ghi đè về nhóm)</string>
|
||||
<string name="disappearing_prohibited_in_this_chat">Tin nhắn tự xóa bị cấm trong cuộc hội thoại này.</string>
|
||||
<string name="no_call_on_lock_screen">Tắt</string>
|
||||
<string name="receipts_groups_title_disable">Tắt chỉ báo đã nhận cho nhóm?</string>
|
||||
<string name="send_receipts_disabled">đã bị tắt</string>
|
||||
<string name="app_check_for_updates_disabled">Đã bị tắt</string>
|
||||
<string name="app_check_for_updates_notice_disable">Tắt</string>
|
||||
<string name="member_info_member_disabled">đã bị tắt</string>
|
||||
<string name="disappearing_messages_are_prohibited">Tin nhắn tự xóa bị cấm trong nhóm này.</string>
|
||||
</resources>
|
||||
@@ -1880,10 +1880,10 @@
|
||||
<string name="smp_servers_other">其他 SMP 服务器</string>
|
||||
<string name="xftp_servers_other">其他 XFTP 服务器</string>
|
||||
<string name="scan_paste_link">扫描/粘贴链接</string>
|
||||
<string name="subscription_percentage">订阅百分比</string>
|
||||
<string name="subscription_percentage">显示百分比</string>
|
||||
<string name="member_info_member_inactive">不活跃</string>
|
||||
<string name="appearance_zoom">缩放</string>
|
||||
<string name="all_users">所有用户</string>
|
||||
<string name="all_users">所有配置文件</string>
|
||||
<string name="servers_info_files_tab">文件</string>
|
||||
<string name="servers_info_missing">没有信息,试试重新加载</string>
|
||||
<string name="servers_info">服务器信息</string>
|
||||
@@ -1891,7 +1891,7 @@
|
||||
<string name="servers_info_sessions_connected">已连接</string>
|
||||
<string name="servers_info_connected_servers_section_header">已连接的服务器</string>
|
||||
<string name="servers_info_sessions_connecting">连接中</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">订阅的连接</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">活跃连接</string>
|
||||
<string name="servers_info_detailed_statistics">详细统计数据</string>
|
||||
<string name="servers_info_details">详情</string>
|
||||
<string name="servers_info_downloaded">已下载</string>
|
||||
@@ -1901,7 +1901,7 @@
|
||||
<string name="servers_info_reset_stats_alert_error_title">重设统计数据出错</string>
|
||||
<string name="servers_info_sessions_errors">错误</string>
|
||||
<string name="servers_info_messages_received">收到的消息</string>
|
||||
<string name="servers_info_subscriptions_section_header">消息订阅</string>
|
||||
<string name="servers_info_subscriptions_section_header">消息接收</string>
|
||||
<string name="servers_info_subscriptions_connections_pending">待连接</string>
|
||||
<string name="servers_info_previously_connected_servers_section_header">先前连接的服务器</string>
|
||||
<string name="servers_info_proxied_servers_section_header">已代理的服务器</string>
|
||||
@@ -1962,7 +1962,7 @@
|
||||
<string name="subscription_results_ignored">订阅被忽略</string>
|
||||
<string name="smp_servers_configured">已配置的 SMP 服务器</string>
|
||||
<string name="xftp_servers_configured">已配置的 XFTP 服务器</string>
|
||||
<string name="current_user">当前用户</string>
|
||||
<string name="current_user">当前配置文件</string>
|
||||
<string name="servers_info_transport_sessions_section_header">传输会话</string>
|
||||
<string name="servers_info_uploaded">已上传</string>
|
||||
<string name="member_info_member_disabled">已停用</string>
|
||||
@@ -1989,4 +1989,15 @@
|
||||
<string name="app_check_for_updates_beta">测试版</string>
|
||||
<string name="app_check_for_updates_installed_successfully_title">安装成功</string>
|
||||
<string name="app_check_for_updates_button_install">安装更新</string>
|
||||
<string name="proxy_destination_error_broker_version">%1$s 的目的地服务器版本不兼容转发服务器 %2$s.</string>
|
||||
<string name="proxy_destination_error_failed_to_connect">转发服务器 %1$s 连接目的地服务器 %2$s 失败。请稍后尝试。</string>
|
||||
<string name="smp_proxy_error_broker_host">转发服务器地址不兼容网络设置:%1$s。</string>
|
||||
<string name="smp_proxy_error_broker_version">转发服务器版本不兼容网络设置:%1$s。</string>
|
||||
<string name="proxy_destination_error_broker_host">%1$s 的目的地服务器地址不兼容转发服务器 %2$s 的设置</string>
|
||||
<string name="smp_proxy_error_connecting">连接转发服务器 %1$s 出错。请稍后尝试。</string>
|
||||
<string name="privacy_media_blur_radius">模糊媒体文件</string>
|
||||
<string name="privacy_media_blur_radius_medium">中度</string>
|
||||
<string name="privacy_media_blur_radius_off">关闭</string>
|
||||
<string name="privacy_media_blur_radius_soft">轻柔</string>
|
||||
<string name="privacy_media_blur_radius_strong">强烈</string>
|
||||
</resources>
|
||||
+7
-16
@@ -165,23 +165,14 @@ actual fun PlatformTextField(
|
||||
},
|
||||
cursorBrush = SolidColor(MaterialTheme.colors.secondary),
|
||||
decorationBox = { innerTextField ->
|
||||
Surface(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colors.secondary),
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
Row(
|
||||
Modifier.background(MaterialTheme.colors.background),
|
||||
verticalAlignment = Alignment.Bottom
|
||||
Row(verticalAlignment = Alignment.Bottom) {
|
||||
CompositionLocalProvider(
|
||||
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current
|
||||
) {
|
||||
Column(Modifier.weight(1f).padding(start = 12.dp, end = 32.dp)) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
innerTextField()
|
||||
Spacer(Modifier.height(10.dp))
|
||||
}
|
||||
Column(Modifier.weight(1f).padding(start = 12.dp, end = 32.dp)) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
innerTextField()
|
||||
Spacer(Modifier.height(10.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -52,9 +52,11 @@ actual fun LazyColumnWithScrollBar(
|
||||
}
|
||||
}
|
||||
}
|
||||
LazyColumn(modifier.then(if (appPlatform.isDesktop) scrollModifier else Modifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content)
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) {
|
||||
DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout)
|
||||
Box {
|
||||
LazyColumn(modifier.then(if (appPlatform.isDesktop) scrollModifier else Modifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content)
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) {
|
||||
DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -13,6 +13,7 @@ actual fun SimpleAndAnimatedImageView(
|
||||
imageBitmap: ImageBitmap,
|
||||
file: CIFile?,
|
||||
imageProvider: () -> ImageGalleryProvider,
|
||||
smallView: Boolean,
|
||||
ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit
|
||||
) {
|
||||
// LALAL make it animated too
|
||||
|
||||
+1
@@ -35,6 +35,7 @@ actual fun ChatListNavLinkLayout(
|
||||
disabled: Boolean,
|
||||
selectedChat: State<Boolean>,
|
||||
nextChatSelected: State<Boolean>,
|
||||
oneHandUI: State<Boolean>,
|
||||
) {
|
||||
var modifier = Modifier.fillMaxWidth()
|
||||
if (!disabled) modifier = modifier
|
||||
|
||||
+57
-43
@@ -22,53 +22,67 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Composable
|
||||
actual fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow<AnimatedViewState>) {
|
||||
// if (call.callState == CallState.Connected && !newChatSheetState.collectAsState().value.isVisible()) {
|
||||
if (!newChatSheetState.collectAsState().value.isVisible()) {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val media = call.peerMedia ?: call.localMedia
|
||||
CompositionLocalProvider(
|
||||
LocalIndication provides NoIndication
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.BottomEnd
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(end = 71.dp, bottom = 92.dp)
|
||||
.size(67.dp)
|
||||
.combinedClickable(onClick = {
|
||||
val chat = chatModel.getChat(call.contact.id)
|
||||
if (chat != null) {
|
||||
withBGApi {
|
||||
openChat(chat.remoteHostId, chat.chatInfo, chatModel)
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongClick = { showMenu.value = true })
|
||||
.onRightClick { showMenu.value = true },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(Modifier.background(MaterialTheme.colors.background, CircleShape)) {
|
||||
ProfileImageForActiveCall(size = 56.dp, image = call.contact.profile.image)
|
||||
}
|
||||
Box(Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp).align(Alignment.TopEnd)) {
|
||||
if (media == CallMediaType.Video) {
|
||||
Icon(painterResource(MR.images.ic_videocam_filled), stringResource(MR.strings.icon_descr_video_call), Modifier.size(18.dp), tint = Color.White)
|
||||
} else {
|
||||
Icon(painterResource(MR.images.ic_call_filled), stringResource(MR.strings.icon_descr_audio_call), Modifier.size(18.dp), tint = Color.White)
|
||||
actual fun ActiveCallInteractiveArea(call: Call) {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val media = call.peerMedia ?: call.localMedia
|
||||
CompositionLocalProvider(
|
||||
LocalIndication provides NoIndication
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.BottomEnd
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(end = 71.dp, bottom = 92.dp)
|
||||
.size(67.dp)
|
||||
.combinedClickable(onClick = {
|
||||
val chat = chatModel.getChat(call.contact.id)
|
||||
if (chat != null) {
|
||||
withBGApi {
|
||||
openChat(chat.remoteHostId, chat.chatInfo, chatModel)
|
||||
}
|
||||
}
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_hang_up), painterResource(MR.images.ic_call_end_filled), color = MaterialTheme.colors.error, onClick = {
|
||||
withBGApi { chatModel.callManager.endCall(call) }
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
},
|
||||
onLongClick = { showMenu.value = true })
|
||||
.onRightClick { showMenu.value = true },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(Modifier.background(MaterialTheme.colors.background, CircleShape)) {
|
||||
ProfileImageForActiveCall(size = 56.dp, image = call.contact.profile.image)
|
||||
}
|
||||
Box(
|
||||
Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
) {
|
||||
if (media == CallMediaType.Video) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_videocam_filled),
|
||||
stringResource(MR.strings.icon_descr_video_call),
|
||||
Modifier.size(18.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_call_filled),
|
||||
stringResource(MR.strings.icon_descr_audio_call),
|
||||
Modifier.size(18.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.icon_descr_hang_up),
|
||||
painterResource(MR.images.ic_call_end_filled),
|
||||
color = MaterialTheme.colors.error,
|
||||
onClick = {
|
||||
withBGApi { chatModel.callManager.endCall(call) }
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user