Merge branch 'master' into master-android

This commit is contained in:
Evgeny Poberezkin
2024-08-06 19:55:42 +01:00
174 changed files with 10361 additions and 13107 deletions
@@ -11,11 +11,11 @@ import android.view.ViewGroup
import android.view.inputmethod.*
import android.widget.EditText
import android.widget.TextView
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
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.toArgb
@@ -51,6 +51,7 @@ actual fun PlatformTextField(
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean,
placeholder: String,
onMessageChange: (String) -> Unit,
onUpArrow: () -> Unit,
onFilesPasted: (List<URI>) -> Unit,
@@ -58,10 +59,11 @@ actual fun PlatformTextField(
) {
val cs = composeState.value
val textColor = MaterialTheme.colors.onBackground
val padding = PaddingValues(12.dp, 7.dp, 45.dp, 0.dp)
val paddingStart = with(LocalDensity.current) { 12.dp.roundToPx() }
val hintColor = MaterialTheme.colors.secondary
val padding = PaddingValues(0.dp, 7.dp, 50.dp, 0.dp)
val paddingStart = 0
val paddingTop = with(LocalDensity.current) { 7.dp.roundToPx() }
val paddingEnd = with(LocalDensity.current) { 45.dp.roundToPx() }
val paddingEnd = with(LocalDensity.current) { 50.dp.roundToPx() }
val paddingBottom = with(LocalDensity.current) { 7.dp.roundToPx() }
var showKeyboard by remember { mutableStateOf(false) }
var freeFocus by remember { mutableStateOf(false) }
@@ -113,6 +115,8 @@ actual fun PlatformTextField(
editText.background = ColorDrawable(Color.Transparent.toArgb())
editText.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom)
editText.setText(cs.message)
editText.hint = placeholder
editText.setHintTextColor(hintColor.toArgb())
if (Build.VERSION.SDK_INT >= 29) {
editText.textCursorDrawable?.let { DrawableCompat.setTint(it, CurrentColors.value.colors.secondary.toArgb()) }
} else {
@@ -135,6 +139,8 @@ actual fun PlatformTextField(
editText
}) {
it.setTextColor(textColor.toArgb())
it.setHintTextColor(hintColor.toArgb())
it.hint = placeholder
it.textSize = textStyle.value.fontSize.value * appPrefs.fontScale.get()
it.isFocusable = composeState.value.preview !is ComposePreview.VoicePreview
it.isFocusableInTouchMode = it.isFocusable
@@ -6,7 +6,6 @@ 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.*
@@ -20,14 +19,9 @@ 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 }
@@ -276,7 +276,9 @@ fun AndroidScreen(settingsState: SettingsViewState) {
snapshotFlow { ModalManager.center.modalCount.value > 0 }
.filter { chatModel.chatId.value == null }
.collect { modalBackground ->
if (modalBackground && !chatModel.newChatSheetVisible.value) {
if (chatModel.newChatSheetVisible.value) {
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, appPrefs.oneHandUI.get())
} else if (modalBackground) {
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())
@@ -345,6 +345,9 @@ object ChatModel {
return withContext(Dispatchers.Main) {
// update current chat
if (chatId.value == cInfo.id) {
if (cItem.isDeletedContent || cItem.meta.itemDeleted != null) {
AudioPlayer.stop(cItem)
}
val items = chatItems.value
val itemIndex = items.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) {
@@ -467,6 +470,21 @@ object ChatModel {
// update current chat
return if (chatId.value == groupInfo.id) {
val memberIndex = groupMembersIndexes[member.groupMemberId]
val updated = chatItems.value.map {
// Take into account only specific changes, not all. Other member updates are not important and can be skipped
if (it.chatDir is CIDirection.GroupRcv && it.chatDir.groupMember.groupMemberId == member.groupMemberId &&
(it.chatDir.groupMember.image != member.image ||
it.chatDir.groupMember.chatViewName != member.chatViewName ||
it.chatDir.groupMember.blocked != member.blocked ||
it.chatDir.groupMember.memberRole != member.memberRole)
)
it.copy(chatDir = CIDirection.GroupRcv(member))
else
it
}
if (updated != chatItems.value) {
chatItems.replaceAll(updated)
}
if (memberIndex != null) {
groupMembers[memberIndex] = member
false
@@ -1909,7 +1927,7 @@ data class ChatItem (
}
}
fun memberToModerate(chatInfo: ChatInfo): Pair<GroupInfo, GroupMember>? {
fun memberToModerate(chatInfo: ChatInfo): Pair<GroupInfo, GroupMember?>? {
return if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupRcv) {
val m = chatInfo.groupInfo.membership
if (m.memberRole >= GroupMemberRole.Admin && m.memberRole >= chatDir.groupMember.memberRole && meta.itemDeleted == null) {
@@ -1917,11 +1935,30 @@ data class ChatItem (
} else {
null
}
} else if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupSnd) {
val m = chatInfo.groupInfo.membership
if (m.memberRole >= GroupMemberRole.Admin) {
chatInfo.groupInfo to null
} else {
null
}
} else {
null
}
}
val showLocalDelete: Boolean
get() = when (content) {
is CIContent.SndDirectE2EEInfo -> false
is CIContent.RcvDirectE2EEInfo -> false
is CIContent.SndGroupE2EEInfo -> false
is CIContent.RcvGroupE2EEInfo -> false
else -> true
}
val canBeDeletedForSelf: Boolean
get() = (content.msgContent != null && !meta.isLive) || meta.itemDeleted != null || isDeletedContent || mergeCategory != null || showLocalDelete
val showNotification: Boolean get() =
when (content) {
is CIContent.SndMsgContent -> false
@@ -142,8 +142,8 @@ class AppPreferences {
},
set = fun(mode: TransportSessionMode) { _networkSessionMode.set(mode.name) }
)
val networkSMPProxyMode = mkStrPreference(SHARED_PREFS_NETWORK_SMP_PROXY_MODE, SMPProxyMode.Never.name)
val networkSMPProxyFallback = mkStrPreference(SHARED_PREFS_NETWORK_SMP_PROXY_FALLBACK, SMPProxyFallback.Allow.name)
val networkSMPProxyMode = mkStrPreference(SHARED_PREFS_NETWORK_SMP_PROXY_MODE, NetCfg.defaults.smpProxyMode.name)
val networkSMPProxyFallback = mkStrPreference(SHARED_PREFS_NETWORK_SMP_PROXY_FALLBACK, NetCfg.defaults.smpProxyFallback.name)
val networkHostMode = mkStrPreference(SHARED_PREFS_NETWORK_HOST_MODE, HostMode.OnionViaSocks.name)
val networkRequiredHostMode = mkBoolPreference(SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE, false)
val networkTCPConnectTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT, NetCfg.defaults.tcpConnectTimeout, NetCfg.proxyDefaults.tcpConnectTimeout)
@@ -225,7 +225,7 @@ class AppPreferences {
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)
val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, appPlatform.isAndroid)
private fun mkIntPreference(prefName: String, default: Int) =
SharedPreference(
@@ -657,8 +657,8 @@ object ChatController {
return null
}
suspend fun apiCreateActiveUser(rh: Long?, p: Profile?, sameServers: Boolean = false, pastTimestamp: Boolean = false, ctrl: ChatCtrl? = null): User? {
val r = sendCmd(rh, CC.CreateActiveUser(p, sameServers = sameServers, pastTimestamp = pastTimestamp), ctrl)
suspend fun apiCreateActiveUser(rh: Long?, p: Profile?, pastTimestamp: Boolean = false, ctrl: ChatCtrl? = null): User? {
val r = sendCmd(rh, CC.CreateActiveUser(p, pastTimestamp = pastTimestamp), ctrl)
if (r is CR.ActiveUser) return r.user.updateRemoteHostId(rh)
else if (
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.DuplicateName ||
@@ -2176,7 +2176,10 @@ object ChatController {
r.chatItemDeletions.forEach { (deletedChatItem, toChatItem) ->
val cInfo = deletedChatItem.chatInfo
val cItem = deletedChatItem.chatItem
AudioPlayer.stop(cItem)
if (chatModel.chatId.value != null) {
// Stop voice playback only inside a chat, allow to play in a chat list
AudioPlayer.stop(cItem)
}
val isLastChatItem = chatModel.getChat(cInfo.id)?.chatItems?.lastOrNull()?.id == cItem.id
if (isLastChatItem && ntfManager.hasNotificationsForChat(cInfo.id)) {
ntfManager.cancelNotificationsForChat(cInfo.id)
@@ -2821,7 +2824,7 @@ class SharedPreference<T>(val get: () -> T, set: (T) -> Unit) {
sealed class CC {
class Console(val cmd: String): CC()
class ShowActiveUser: CC()
class CreateActiveUser(val profile: Profile?, val sameServers: Boolean, val pastTimestamp: Boolean): CC()
class CreateActiveUser(val profile: Profile?, val pastTimestamp: Boolean): CC()
class ListUsers: CC()
class ApiSetActiveUser(val userId: Long, val viewPwd: String?): CC()
class SetAllContactReceipts(val enable: Boolean): CC()
@@ -2959,7 +2962,7 @@ sealed class CC {
is Console -> cmd
is ShowActiveUser -> "/u"
is CreateActiveUser -> {
val user = NewUser(profile, sameServers = sameServers, pastTimestamp = pastTimestamp)
val user = NewUser(profile, pastTimestamp = pastTimestamp)
"/_create user ${json.encodeToString(user)}"
}
is ListUsers -> "/users"
@@ -3290,7 +3293,6 @@ fun onOff(b: Boolean): String = if (b) "on" else "off"
@Serializable
data class NewUser(
val profile: Profile?,
val sameServers: Boolean,
val pastTimestamp: Boolean
)
@@ -3586,10 +3588,6 @@ enum class SMPProxyMode {
@SerialName("unknown") Unknown,
@SerialName("unprotected") Unprotected,
@SerialName("never") Never;
companion object {
val default = Never
}
}
@Serializable
@@ -3597,10 +3595,6 @@ enum class SMPProxyFallback {
@SerialName("allow") Allow,
@SerialName("allowProtected") AllowProtected,
@SerialName("prohibit") Prohibit;
companion object {
val default = Allow
}
}
@Serializable
@@ -6227,7 +6221,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) }
oneHandUI?.let { def.oneHandUI.set(if (appPlatform.isAndroid) it else false) }
}
companion object {
@@ -6259,7 +6253,7 @@ data class AppSettings(
uiDarkColorScheme = DefaultTheme.SIMPLEX.themeName,
uiCurrentThemeIds = null,
uiThemes = null,
oneHandUI = false
oneHandUI = true
)
val current: AppSettings
@@ -14,6 +14,7 @@ expect fun PlatformTextField(
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean,
placeholder: String,
onMessageChange: (String) -> Unit,
onUpArrow: () -> Unit,
onFilesPasted: (List<URI>) -> Unit,
@@ -20,6 +20,8 @@ import chat.simplex.common.views.chat.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
@Composable
fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
@@ -77,29 +79,33 @@ fun TerminalLayout(
Scaffold(
topBar = { CloseSheetBar(close) },
bottomBar = {
Box(Modifier.padding(horizontal = 8.dp)) {
SendMsgView(
composeState = composeState,
showVoiceRecordIcon = false,
recState = remember { mutableStateOf(RecordingState.NotStarted) },
isDirectChat = false,
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
sendMsgEnabled = true,
sendButtonEnabled = true,
nextSendGrpInv = false,
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = false,
userIsObserver = false,
userCanSend = true,
allowVoiceToContact = {},
sendMessage = { sendCommand() },
sendLiveMessage = null,
updateLiveMessage = null,
editPrevMessage = {},
onMessageChange = ::onMessageChange,
onFilesPasted = {},
textStyle = textStyle
)
Column {
Divider()
Box(Modifier.padding(horizontal = 8.dp)) {
SendMsgView(
composeState = composeState,
showVoiceRecordIcon = false,
recState = remember { mutableStateOf(RecordingState.NotStarted) },
isDirectChat = false,
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
sendMsgEnabled = true,
sendButtonEnabled = true,
nextSendGrpInv = false,
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = false,
userIsObserver = false,
userCanSend = true,
allowVoiceToContact = {},
placeholder = "",
sendMessage = { sendCommand() },
sendLiveMessage = null,
updateLiveMessage = null,
editPrevMessage = {},
onMessageChange = ::onMessageChange,
onFilesPasted = {},
textStyle = textStyle
)
}
}
},
contentColor = LocalContentColor.current,
@@ -28,6 +28,7 @@ import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.chat.item.MarkdownText
import chat.simplex.common.views.helpers.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.group.MemberProfileImage
import chat.simplex.common.views.chatlist.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
@@ -334,7 +335,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
SectionItemView(
padding = PaddingValues(horizontal = 0.dp)
) {
ProfileImage(size = 36.dp, member.image)
MemberProfileImage(size = 36.dp, member)
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON))
Text(
member.chatViewName,
@@ -1,5 +1,7 @@
package chat.simplex.common.views.chat
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.*
@@ -50,13 +52,14 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
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
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
}
return@derivedStateOf info ?: ChatInfo.Direct.sampleData
}
}
val user = chatModel.currentUser.value
if (shouldReturn.value || user == null) {
@@ -82,6 +85,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
val attachmentOption = rememberSaveable { mutableStateOf<AttachmentOption?>(null) }
val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
val selectedChatItems = rememberSaveable { mutableStateOf(null as Set<Long>?) }
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
@@ -95,8 +99,12 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
}
}
}
KeyChangeEffect(chatModel.chatId.value) {
if (chatModel.chatId.value != null) {
selectedChatItems.value = null
}
}
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)
@@ -117,26 +125,61 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
unreadCount,
composeState,
composeView = {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (
chatInfo is ChatInfo.Direct
&& !chatInfo.contact.sndReady
&& chatInfo.contact.active
&& !chatInfo.contact.nextSendGrpInv
if (selectedChatItems.value == null) {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
generalGetString(MR.strings.contact_connection_pending),
Modifier.padding(top = 4.dp),
fontSize = 14.sp,
color = MaterialTheme.colors.secondary
if (
chatInfo is ChatInfo.Direct
&& !chatInfo.contact.sndReady
&& chatInfo.contact.active
&& !chatInfo.contact.nextSendGrpInv
) {
Text(
generalGetString(MR.strings.contact_connection_pending),
Modifier.padding(top = 4.dp),
fontSize = 14.sp,
color = MaterialTheme.colors.secondary
)
}
ComposeView(
chatModel, Chat(remoteHostId = chatRh, chatInfo = chatInfo, chatItems = emptyList()), composeState, attachmentOption,
showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } }
)
}
ComposeView(
chatModel, Chat(remoteHostId = chatRh, chatInfo = chatInfo, chatItems = emptyList()), composeState, attachmentOption,
showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } }
} else {
SelectedItemsBottomToolbar(
chatItems = remember { chatModel.chatItems }.value,
selectedChatItems = selectedChatItems,
chatInfo = chatInfo,
deleteItems = { canDeleteForAll ->
val itemIds = selectedChatItems.value
if (itemIds != null) {
deleteMessagesAlertDialog(
itemIds.sorted(),
generalGetString(if (itemIds.size == 1) MR.strings.delete_message_mark_deleted_warning else MR.strings.delete_messages_mark_deleted_warning),
forAll = canDeleteForAll,
deleteMessages = { ids, forAll ->
deleteMessages(chatRh, chatInfo, ids, forAll, moderate = false) {
selectedChatItems.value = null
}
}
)
}
},
moderateItems = {
if (chatInfo is ChatInfo.Group) {
val itemIds = selectedChatItems.value
if (itemIds != null) {
moderateMessagesAlertDialog(itemIds.sorted(), moderateMessageQuestionText(chatInfo.featureEnabled(ChatFeature.FullDelete), itemIds.size), deleteMessages = { ids ->
deleteMessages(chatRh, chatInfo, ids, true, moderate = true) {
selectedChatItems.value = null
}
})
}
}
}
)
}
},
@@ -145,6 +188,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
searchText,
useLinkPreviews = useLinkPreviews,
linkMode = chatModel.simplexLinkMode.value,
selectedChatItems = selectedChatItems,
back = {
hideKeyboard(view)
AudioPlayer.stop()
@@ -271,22 +315,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
}
}
},
deleteMessages = { itemIds ->
if (itemIds.isNotEmpty()) {
withBGApi {
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)
}
}
}
}
}
},
deleteMessages = { itemIds -> deleteMessages(chatRh, chatInfo, itemIds, false, moderate = false) },
receiveFile = { fileId ->
withBGApi { chatModel.controller.receiveFile(chatRh, user, fileId) }
},
@@ -478,7 +507,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
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)
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, backgroundColorState.value, true, false)
}
}
}
@@ -536,6 +565,7 @@ fun ChatLayout(
searchValue: State<String>,
useLinkPreviews: Boolean,
linkMode: SimplexLinkMode,
selectedChatItems: MutableState<Set<Long>?>,
back: () -> Unit,
info: () -> Unit,
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
@@ -593,9 +623,11 @@ fun ChatLayout(
)
) {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
val elevation = remember { derivedStateOf { if (attachmentBottomSheetState.currentValue == ModalBottomSheetValue.Hidden) 0.dp else ModalBottomSheetDefaults.Elevation } }
ModalBottomSheetLayout(
scrimColor = Color.Black.copy(alpha = 0.12F),
modifier = Modifier.navigationBarsWithImePadding(),
sheetElevation = elevation.value,
sheetContent = {
ChooseAttachmentView(
attachmentOption,
@@ -611,7 +643,13 @@ fun ChatLayout(
}
Scaffold(
topBar = { ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) },
topBar = {
if (selectedChatItems.value == null) {
ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
} else {
SelectedItemsTopToolbar(selectedChatItems)
}
},
bottomBar = composeView,
modifier = Modifier.navigationBarsWithImePadding(),
floatingActionButton = { floatingButton.value() },
@@ -634,7 +672,7 @@ fun ChatLayout(
) {
ChatItemsList(
remoteHostId, chatInfo, unreadCount, composeState, searchValue,
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy,
@@ -899,6 +937,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
searchValue: State<String>,
useLinkPreviews: Boolean,
linkMode: SimplexLinkMode,
selectedChatItems: MutableState<Set<Long>?>,
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
loadPrevMessages: () -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit,
@@ -1012,86 +1051,117 @@ fun BoxWithConstraintsScope.ChatItemsList(
tryOrShowError("${cItem.id}ChatItem", error = {
CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
}) {
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)
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, 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 (chatInfo is ChatInfo.Group) {
if (cItem.chatDir is CIDirection.GroupRcv) {
val member = cItem.chatDir.groupMember
val (prevMember, memCount) =
if (range != null) {
chatModel.getPrevHiddenMember(member, range)
} else {
null to 1
}
if (prevItem == null || showMemberImage(member, prevItem) || prevMember != null) {
Column(
Modifier
.padding(top = 8.dp)
.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalAlignment = Alignment.Start
) {
if (cItem.content.showMemberName) {
val memberNameStyle = SpanStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary)
val memberNameString = if (memCount == 1 && member.memberRole > GroupMemberRole.Member) {
buildAnnotatedString {
withStyle(memberNameStyle.copy(fontWeight = FontWeight.Medium)) { append(member.memberRole.text) }
append(" ")
withStyle(memberNameStyle) { append(memberNames(member, prevMember, memCount)) }
}
} else {
buildAnnotatedString {
withStyle(memberNameStyle) { append(memberNames(member, prevMember, memCount)) }
}
}
Text(
memberNameString,
Modifier.padding(start = MEMBER_IMAGE_SIZE + 10.dp),
maxLines = 2
)
val sent = cItem.chatDir.sent
Box(Modifier.padding(bottom = 4.dp)) {
val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null
val selectionVisible = selectedChatItems.value != null && cItem.canBeDeletedForSelf
val selectionOffset by animateDpAsState(if (selectionVisible && !sent) 4.dp + 22.dp * fontSizeMultiplier else 0.dp)
val swipeableOrSelectionModifier = (if (selectionVisible) Modifier else swipeableModifier).graphicsLayer { translationX = selectionOffset.toPx() }
if (chatInfo is ChatInfo.Group) {
if (cItem.chatDir is CIDirection.GroupRcv) {
val member = cItem.chatDir.groupMember
val (prevMember, memCount) =
if (range != null) {
chatModel.getPrevHiddenMember(member, range)
} else {
null to 1
}
Row(
swipeableModifier,
horizontalArrangement = Arrangement.spacedBy(4.dp)
if (prevItem == null || showMemberImage(member, prevItem) || prevMember != null) {
Column(
Modifier
.padding(top = 8.dp)
.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalAlignment = Alignment.Start
) {
Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) {
MemberImage(member)
if (cItem.content.showMemberName) {
val memberNameStyle = SpanStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary)
val memberNameString = if (memCount == 1 && member.memberRole > GroupMemberRole.Member) {
buildAnnotatedString {
withStyle(memberNameStyle.copy(fontWeight = FontWeight.Medium)) { append(member.memberRole.text) }
append(" ")
withStyle(memberNameStyle) { append(memberNames(member, prevMember, memCount)) }
}
} else {
buildAnnotatedString {
withStyle(memberNameStyle) { append(memberNames(member, prevMember, memCount)) }
}
}
Text(
memberNameString,
Modifier.padding(start = MEMBER_IMAGE_SIZE + 10.dp),
maxLines = 2
)
}
Box(contentAlignment = Alignment.CenterStart) {
androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
SelectedChatItem(Modifier, cItem.id, selectedChatItems)
}
Row(
swipeableOrSelectionModifier,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) {
MemberImage(member)
}
ChatItemViewShortHand(cItem, range)
}
}
}
} else {
Box(contentAlignment = Alignment.CenterStart) {
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
}
Row(
Modifier
.padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp)
.then(swipeableOrSelectionModifier)
) {
ChatItemViewShortHand(cItem, range)
}
ChatItemViewShortHand(cItem, range)
}
}
} else {
Row(
Modifier
.padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp)
.then(swipeableModifier)
Box(contentAlignment = Alignment.CenterStart) {
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
}
Box(
Modifier
.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp)
.then(if (selectionVisible) Modifier else swipeableModifier)
) {
ChatItemViewShortHand(cItem, range)
}
}
}
} else { // direct message
Box(contentAlignment = Alignment.CenterStart) {
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
}
Box(
Modifier.padding(
start = if (sent && !voiceWithTransparentBack) 76.dp else 12.dp,
end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp,
).then(if (!selectionVisible || !sent) swipeableOrSelectionModifier else Modifier)
) {
ChatItemViewShortHand(cItem, range)
}
}
} else {
Box(
Modifier
.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp)
.then(swipeableModifier)
) {
ChatItemViewShortHand(cItem, range)
}
}
} else { // direct message
val sent = cItem.chatDir.sent
Box(
Modifier.padding(
start = if (sent && !voiceWithTransparentBack) 76.dp else 12.dp,
end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp,
).then(swipeableModifier)
) {
ChatItemViewShortHand(cItem, range)
if (selectionVisible) {
Box(Modifier.matchParentSize().clickable {
val checked = selectedChatItems.value?.contains(cItem.id) == true
selectUnselectChatItem(select = !checked, cItem, revealed, selectedChatItems)
})
}
}
}
@@ -1317,7 +1387,7 @@ val MEMBER_IMAGE_SIZE: Dp = 38.dp
@Composable
fun MemberImage(member: GroupMember) {
ProfileImage(MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier, member.memberProfile.image, backgroundColor = MaterialTheme.colors.background)
MemberProfileImage(MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier, member, backgroundColor = MaterialTheme.colors.background)
}
@Composable
@@ -1416,6 +1486,96 @@ private fun bottomEndFloatingButton(
}
}
@Composable
private fun SelectedChatItem(
modifier: Modifier,
ciId: Long,
selectedChatItems: State<Set<Long>?>,
) {
val checked = remember { derivedStateOf { selectedChatItems.value?.contains(ciId) == true } }
Icon(
painterResource(if (checked.value) MR.images.ic_check_circle_filled else MR.images.ic_radio_button_unchecked),
null,
modifier.size(22.dp * fontSizeMultiplier),
tint = if (checked.value) {
MaterialTheme.colors.primary
} else if (isInDarkTheme()) {
// .tertiaryLabel instead of .secondary
Color(red = 235f / 255f, 235f / 255f, 245f / 255f, 76f / 255f)
} else {
// .tertiaryLabel instead of .secondary
Color(red = 60f / 255f, 60f / 255f, 67f / 255f, 76f / 255f)
}
)
}
private fun selectUnselectChatItem(select: Boolean, ci: ChatItem, revealed: State<Boolean>, selectedChatItems: MutableState<Set<Long>?>) {
val itemIds = mutableSetOf<Long>()
if (!revealed.value) {
val currIndex = chatModel.getChatItemIndexOrNull(ci)
val ciCategory = ci.mergeCategory
if (currIndex != null && ciCategory != null) {
val (prevHidden, _) = chatModel.getPrevShownChatItem(currIndex, ciCategory)
val range = chatViewItemsRange(currIndex, prevHidden)
if (range != null) {
val reversedChatItems = chatModel.chatItems.asReversed()
for (i in range) {
itemIds.add(reversedChatItems[i].id)
}
} else {
itemIds.add(ci.id)
}
} else {
itemIds.add(ci.id)
}
} else {
itemIds.add(ci.id)
}
if (select) {
val sel = selectedChatItems.value ?: setOf()
selectedChatItems.value = sel.union(itemIds)
} else {
val sel = (selectedChatItems.value ?: setOf()).toMutableSet()
sel.removeAll(itemIds)
selectedChatItems.value = sel
}
}
private fun deleteMessages(chatRh: Long?, chatInfo: ChatInfo, itemIds: List<Long>, forAll: Boolean, moderate: Boolean, onSuccess: () -> Unit = {}) {
if (itemIds.isNotEmpty()) {
withBGApi {
val deleted = if (chatInfo is ChatInfo.Group && forAll && moderate) {
chatModel.controller.apiDeleteMemberChatItems(
chatRh,
groupId = chatInfo.groupInfo.groupId,
itemIds = itemIds
)
} else {
chatModel.controller.apiDeleteChatItems(
chatRh,
type = chatInfo.chatType,
id = chatInfo.apiId,
itemIds = itemIds,
mode = if (forAll) CIDeleteMode.cidmBroadcast else CIDeleteMode.cidmInternal
)
}
if (deleted != null) {
withChats {
for (di in deleted) {
val toChatItem = di.toChatItem?.chatItem
if (toChatItem != null) {
upsertChatItem(chatRh, chatInfo, toChatItem)
} else {
removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
}
}
}
onSuccess()
}
}
}
}
private fun markUnreadChatAsRead(chatId: String) {
val chat = chatModel.chats.value.firstOrNull { it.id == chatId }
if (chat?.chatStats?.unreadChat != true) return
@@ -1593,6 +1753,7 @@ fun PreviewChatLayout() {
searchValue,
useLinkPreviews = true,
linkMode = SimplexLinkMode.DESCRIPTION,
selectedChatItems = remember { mutableStateOf(setOf()) },
back = {},
info = {},
showMemberInfo = { _, _ -> },
@@ -1664,6 +1825,7 @@ fun PreviewGroupChatLayout() {
searchValue,
useLinkPreviews = true,
linkMode = SimplexLinkMode.DESCRIPTION,
selectedChatItems = remember { mutableStateOf(setOf()) },
back = {},
info = {},
showMemberInfo = { _, _ -> },
@@ -872,11 +872,9 @@ fun ComposeView(
}
}
}
Column(Modifier.background(MaterialTheme.colors.background)) {
Divider()
Row(
modifier = Modifier.background(MaterialTheme.colors.background).padding(end = 8.dp),
verticalAlignment = Alignment.Bottom,
) {
Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) {
val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership)
val attachmentClicked = if (isGroupAndProhibitedFiles) {
{
@@ -896,7 +894,7 @@ fun ComposeView(
&& !nextSendGrpInv.value
IconButton(
attachmentClicked,
Modifier.padding(bottom = if (appPlatform.isAndroid) 2.dp else with(LocalDensity.current) { 7.sp.toDp() }),
Modifier.padding(bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier),
enabled = attachmentEnabled
) {
Icon(
@@ -925,7 +923,7 @@ fun ComposeView(
snapshotFlow { recState.value }
.distinctUntilChanged()
.collect {
when(it) {
when (it) {
is RecordingState.Started -> onAudioAdded(it.filePath, it.progressMs, false)
is RecordingState.Finished -> if (it.durationMs > 300) {
onAudioAdded(it.filePath, it.durationMs, true)
@@ -1005,6 +1003,7 @@ fun ComposeView(
sendButtonColor = sendButtonColor,
timedMessageAllowed = timedMessageAllowed,
customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime,
placeholder = stringResource(MR.strings.compose_message_placeholder),
sendMessage = { ttl ->
sendMessage(ttl)
resetLinkPreview()
@@ -1022,4 +1021,5 @@ fun ComposeView(
)
}
}
}
}
@@ -0,0 +1,132 @@
package chat.simplex.common.views.chat
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.draw.alpha
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.platform.BackHandler
import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.helpers.*
import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
@Composable
fun SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>) {
val onBackClicked = { selectedChatItems.value = null }
BackHandler(onBack = onBackClicked)
val count = selectedChatItems.value?.size ?: 0
DefaultTopAppBar(
navigationButton = { NavigationButtonClose(onButtonClicked = onBackClicked) },
title = {
Text(
if (count == 0) {
stringResource(MR.strings.selected_chat_items_nothing_selected)
} else {
stringResource(MR.strings.selected_chat_items_selected_n).format(count)
},
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
onTitleClick = null,
showSearch = false,
onSearchValueChanged = {},
)
Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier))
}
@Composable
fun SelectedItemsBottomToolbar(
chatInfo: ChatInfo,
chatItems: List<ChatItem>,
selectedChatItems: MutableState<Set<Long>?>,
deleteItems: (Boolean) -> Unit, // Boolean - delete for everyone is possible
moderateItems: () -> Unit,
// shareItems: () -> Unit,
) {
val deleteEnabled = remember { mutableStateOf(false) }
val deleteForEveryoneEnabled = remember { mutableStateOf(false) }
val canModerate = remember { mutableStateOf(false) }
val moderateEnabled = remember { mutableStateOf(false) }
val allButtonsDisabled = remember { mutableStateOf(false) }
Box {
// It's hard to measure exact height of ComposeView with different fontSizes. Better to depend on actual ComposeView, even empty
ComposeView(chatModel = chatModel, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {})
Row(Modifier.matchParentSize().background(MaterialTheme.colors.background), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
IconButton({ deleteItems(deleteForEveryoneEnabled.value) }, enabled = deleteEnabled.value && !allButtonsDisabled.value) {
Icon(
painterResource(MR.images.ic_delete),
null,
Modifier.size(24.dp),
tint = if (!deleteEnabled.value || allButtonsDisabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error
)
}
IconButton({ moderateItems() }, Modifier.alpha(if (canModerate.value) 1f else 0f), enabled = moderateEnabled.value && !allButtonsDisabled.value) {
Icon(
painterResource(MR.images.ic_flag),
null,
Modifier.size(24.dp),
tint = if (!moderateEnabled.value || allButtonsDisabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error
)
}
IconButton({ /*shareItems()*/ }, Modifier.alpha(0f), enabled = false/*!allButtonsDisabled.value*/) {
Icon(
painterResource(MR.images.ic_share),
null,
Modifier.size(24.dp),
tint = if (allButtonsDisabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
)
}
}
}
LaunchedEffect(chatInfo, chatItems, selectedChatItems.value) {
recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, allButtonsDisabled)
}
}
private fun recheckItems(chatInfo: ChatInfo,
chatItems: List<ChatItem>,
selectedChatItems: MutableState<Set<Long>?>,
deleteEnabled: MutableState<Boolean>,
deleteForEveryoneEnabled: MutableState<Boolean>,
canModerate: MutableState<Boolean>,
moderateEnabled: MutableState<Boolean>,
allButtonsDisabled: MutableState<Boolean>
) {
val count = selectedChatItems.value?.size ?: 0
allButtonsDisabled.value = count == 0 || count > 20
canModerate.value = possibleToModerate(chatInfo)
val selected = selectedChatItems.value ?: return
var rDeleteEnabled = true
var rDeleteForEveryoneEnabled = true
var rModerateEnabled = true
var rOnlyOwnGroupItems = true
val rSelectedChatItems = mutableSetOf<Long>()
for (ci in chatItems) {
if (selected.contains(ci.id)) {
rDeleteEnabled = rDeleteEnabled && ci.canBeDeletedForSelf
rDeleteForEveryoneEnabled = rDeleteForEveryoneEnabled && ci.meta.deletable && !ci.localNote
rOnlyOwnGroupItems = rOnlyOwnGroupItems && ci.chatDir is CIDirection.GroupSnd
rModerateEnabled = rModerateEnabled && !rOnlyOwnGroupItems && ci.content.msgContent != null && ci.memberToModerate(chatInfo) != null
rSelectedChatItems.add(ci.id) // we are collecting new selected items here to account for any changes in chat items list
}
}
deleteEnabled.value = rDeleteEnabled
deleteForEveryoneEnabled.value = rDeleteForEveryoneEnabled
moderateEnabled.value = rModerateEnabled
selectedChatItems.value = rSelectedChatItems
}
private fun possibleToModerate(chatInfo: ChatInfo): Boolean =
chatInfo is ChatInfo.Group && chatInfo.groupInfo.membership.memberRole >= GroupMemberRole.Admin
@@ -15,12 +15,10 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.helpers.*
@@ -51,6 +49,7 @@ fun SendMsgView(
allowVoiceToContact: () -> Unit,
timedMessageAllowed: Boolean = false,
customDisappearingMessageTimePref: SharedPreference<Int>? = null,
placeholder: String,
sendMessage: (Int?) -> Unit,
sendLiveMessage: (suspend () -> Unit)? = null,
updateLiveMessage: (suspend () -> Unit)? = null,
@@ -62,7 +61,7 @@ fun SendMsgView(
) {
val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) }
Box(Modifier.padding(vertical = 8.dp)) {
Box(Modifier.padding(vertical = if (appPlatform.isAndroid) 8.dp else 6.dp)) {
val cs = composeState.value
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(composeState.value.inProgress) {
@@ -80,13 +79,24 @@ fun SendMsgView(
(!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
cs.endLiveDisabled ||
!sendButtonEnabled
PlatformTextField(composeState, sendMsgEnabled, sendMsgButtonDisabled, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage, onFilesPasted) {
val clicksOnTextFieldDisabled = !sendMsgEnabled || cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress
PlatformTextField(
composeState,
sendMsgEnabled,
sendMsgButtonDisabled,
textStyle,
showDeleteTextButton,
userIsObserver,
if (clicksOnTextFieldDisabled) "" else placeholder,
onMessageChange,
editPrevMessage,
onFilesPasted
) {
if (!cs.inProgress) {
sendMessage(null)
}
}
// Disable clicks on text field
if (!sendMsgEnabled || cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress) {
if (clicksOnTextFieldDisabled) {
Box(
Modifier
.matchParentSize()
@@ -101,7 +111,7 @@ fun SendMsgView(
if (showDeleteTextButton.value) {
DeleteTextButton(composeState)
}
Box(Modifier.align(Alignment.BottomEnd).padding(bottom = if (appPlatform.isAndroid) 0.dp else with(LocalDensity.current) { 5.sp.toDp() } * fontSizeSqrtMultiplier)) {
Box(Modifier.align(Alignment.BottomEnd).padding(bottom = if (appPlatform.isAndroid) 0.dp else 5.sp.toDp() * fontSizeSqrtMultiplier)) {
val sendButtonSize = remember { Animatable(36f) }
val sendButtonAlpha = remember { Animatable(1f) }
val scope = rememberCoroutineScope()
@@ -564,6 +574,7 @@ fun PreviewSendMsgView() {
userCanSend = true,
allowVoiceToContact = {},
timedMessageAllowed = false,
placeholder = "",
sendMessage = {},
editPrevMessage = {},
onMessageChange = { _ -> },
@@ -599,6 +610,7 @@ fun PreviewSendMsgViewEditing() {
userCanSend = true,
allowVoiceToContact = {},
timedMessageAllowed = false,
placeholder = "",
sendMessage = {},
editPrevMessage = {},
onMessageChange = { _ -> },
@@ -634,6 +646,7 @@ fun PreviewSendMsgViewInProgress() {
userCanSend = true,
allowVoiceToContact = {},
timedMessageAllowed = false,
placeholder = "",
sendMessage = {},
editPrevMessage = {},
onMessageChange = { _ -> },
@@ -487,7 +487,7 @@ private fun MemberRow(member: GroupMember, user: Boolean = false, onClick: (() -
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
ProfileImage(size = 46.dp, member.image)
MemberProfileImage(size = 46.dp, member)
Spacer(Modifier.width(DEFAULT_PADDING_HALF))
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -25,8 +25,7 @@ import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.withChats
@@ -495,7 +494,7 @@ fun GroupMemberInfoHeader(member: GroupMember) {
Modifier.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
ProfileImage(size = 192.dp, member.image, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
MemberProfileImage(size = 192.dp, member, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
val text = buildAnnotatedString {
if (member.verified) {
appendInlineContent(id = "shieldIcon")
@@ -625,6 +624,22 @@ private fun RoleSelectionRow(
}
}
@Composable
fun MemberProfileImage(
size: Dp,
mem: GroupMember,
color: Color = MaterialTheme.colors.secondaryVariant,
backgroundColor: Color? = null
) {
ProfileImage(
size = size,
image = mem.image,
color = color,
backgroundColor = backgroundColor,
blurred = mem.blocked
)
}
private fun updateMemberRoleDialog(
newRole: GroupMemberRole,
member: GroupMember,
@@ -269,7 +269,7 @@ fun CIImageView(
if (!smallView && (!showDownloadButton(file?.fileStatus) || !blurred.value)) {
loadingIndicator()
} else if (smallView && file?.showStatusIconInSmallView == true) {
Box(Modifier.align(Alignment.Center)) {
Box(Modifier.matchParentSize(), contentAlignment = Alignment.Center) {
loadingIndicator()
}
}
@@ -50,6 +50,8 @@ fun ChatItemView(
linkMode: SimplexLinkMode,
revealed: MutableState<Boolean>,
range: IntRange?,
selectedChatItems: MutableState<Set<Long>?>,
selectChatItem: () -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit,
deleteMessages: (List<Long>) -> Unit,
receiveFile: (Long) -> Unit,
@@ -81,9 +83,7 @@ fun ChatItemView(
val live = composeState.value.liveMessage != null
Box(
modifier = Modifier
.padding(bottom = 4.dp)
.fillMaxWidth(),
modifier = Modifier.fillMaxWidth(),
contentAlignment = alignment,
) {
val info = cItem.meta.itemStatus.statusInto
@@ -142,14 +142,6 @@ fun ChatItemView(
}
}
fun moderateMessageQuestionText(): String {
return if (fullDeleteAllowed) {
generalGetString(MR.strings.moderate_message_will_be_deleted_warning)
} else {
generalGetString(MR.strings.moderate_message_will_be_marked_warning)
}
}
@Composable
fun MsgReactionsMenu() {
val rs = MsgReaction.values.mapNotNull { r ->
@@ -180,6 +172,10 @@ fun ChatItemView(
fun DeleteItemMenu() {
DefaultDropdownMenu(showMenu) {
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
if (cItem.canBeDeletedForSelf) {
Divider()
SelectItemAction(showMenu, selectChatItem)
}
}
}
@@ -277,8 +273,12 @@ fun ChatItemView(
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
}
val groupInfo = cItem.memberToModerate(cInfo)?.first
if (groupInfo != null) {
ModerateItemAction(cItem, questionText = moderateMessageQuestionText(), showMenu, deleteMessage)
if (groupInfo != null && cItem.chatDir !is CIDirection.GroupSnd) {
ModerateItemAction(cItem, questionText = moderateMessageQuestionText(cInfo.featureEnabled(ChatFeature.FullDelete), 1), showMenu, deleteMessage)
}
if (cItem.canBeDeletedForSelf) {
Divider()
SelectItemAction(showMenu, selectChatItem)
}
}
}
@@ -293,12 +293,20 @@ fun ChatItemView(
}
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
if (cItem.canBeDeletedForSelf) {
Divider()
SelectItemAction(showMenu, selectChatItem)
}
}
}
cItem.isDeletedContent -> {
DefaultDropdownMenu(showMenu) {
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
if (cItem.canBeDeletedForSelf) {
Divider()
SelectItemAction(showMenu, selectChatItem)
}
}
}
cItem.mergeCategory != null && ((range?.count() ?: 0) > 1 || revealed.value) -> {
@@ -309,11 +317,19 @@ fun ChatItemView(
ExpandItemAction(revealed, showMenu)
}
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
if (cItem.canBeDeletedForSelf) {
Divider()
SelectItemAction(showMenu, selectChatItem)
}
}
}
else -> {
DefaultDropdownMenu(showMenu) {
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
if (selectedChatItems.value == null) {
Divider()
SelectItemAction(showMenu, selectChatItem)
}
}
}
}
@@ -327,6 +343,10 @@ fun ChatItemView(
}
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
if (cItem.canBeDeletedForSelf) {
Divider()
SelectItemAction(showMenu, selectChatItem)
}
}
}
@@ -357,6 +377,10 @@ fun ChatItemView(
DefaultDropdownMenu(showMenu) {
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
if (cItem.canBeDeletedForSelf) {
Divider()
SelectItemAction(showMenu, selectChatItem)
}
}
}
@@ -410,6 +434,10 @@ fun ChatItemView(
DefaultDropdownMenu(showMenu) {
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages)
if (cItem.canBeDeletedForSelf) {
Divider()
SelectItemAction(showMenu, selectChatItem)
}
}
}
@@ -607,7 +635,12 @@ fun DeleteItemAction(
for (i in range) {
itemIds.add(reversedChatItems[i].id)
}
deleteMessagesAlertDialog(itemIds, generalGetString(MR.strings.delete_message_mark_deleted_warning), deleteMessages = deleteMessages)
deleteMessagesAlertDialog(
itemIds,
generalGetString(if (itemIds.size == 1) MR.strings.delete_message_mark_deleted_warning else MR.strings.delete_messages_mark_deleted_warning),
forAll = false,
deleteMessages = { ids, _ -> deleteMessages(ids) }
)
} else {
deleteMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage)
}
@@ -640,6 +673,21 @@ fun ModerateItemAction(
)
}
@Composable
fun SelectItemAction(
showMenu: MutableState<Boolean>,
selectChatItem: () -> Unit,
) {
ItemAction(
stringResource(MR.strings.select_verb),
painterResource(MR.images.ic_check_circle),
onClick = {
showMenu.value = false
selectChatItem()
}
)
}
@Composable
private fun RevealItemAction(revealed: MutableState<Boolean>, showMenu: MutableState<Boolean>) {
ItemAction(
@@ -784,7 +832,7 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMes
)
}
fun deleteMessagesAlertDialog(itemIds: List<Long>, questionText: String, deleteMessages: (List<Long>) -> Unit) {
fun deleteMessagesAlertDialog(itemIds: List<Long>, questionText: String, forAll: Boolean, deleteMessages: (List<Long>, Boolean) -> Unit) {
AlertManager.shared.showAlertDialogButtons(
title = generalGetString(MR.strings.delete_messages__question).format(itemIds.size),
text = questionText,
@@ -796,14 +844,29 @@ fun deleteMessagesAlertDialog(itemIds: List<Long>, questionText: String, deleteM
horizontalArrangement = Arrangement.Center,
) {
TextButton(onClick = {
deleteMessages(itemIds)
deleteMessages(itemIds, false)
AlertManager.shared.hideAlert()
}) { Text(stringResource(MR.strings.for_me_only), color = MaterialTheme.colors.error) }
if (forAll) {
TextButton(onClick = {
deleteMessages(itemIds, true)
AlertManager.shared.hideAlert()
}) { Text(stringResource(MR.strings.for_everybody), color = MaterialTheme.colors.error) }
}
}
}
)
}
fun moderateMessageQuestionText(fullDeleteAllowed: Boolean, count: Int): String {
return if (fullDeleteAllowed) {
generalGetString(if (count == 1) MR.strings.moderate_message_will_be_deleted_warning else MR.strings.moderate_messages_will_be_deleted_warning)
} else {
generalGetString(if (count == 1) MR.strings.moderate_message_will_be_marked_warning else MR.strings.moderate_messages_will_be_marked_warning)
}
}
fun moderateMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.delete_member_message__question),
@@ -816,6 +879,16 @@ fun moderateMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteM
)
}
fun moderateMessagesAlertDialog(itemIds: List<Long>, questionText: String, deleteMessages: (List<Long>) -> Unit) {
AlertManager.shared.showAlertDialog(
title = if (itemIds.size == 1) generalGetString(MR.strings.delete_member_message__question) else generalGetString(MR.strings.delete_members_messages__question).format(itemIds.size),
text = questionText,
confirmText = generalGetString(MR.strings.delete_verb),
destructive = true,
onConfirm = { deleteMessages(itemIds) }
)
}
expect fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager)
@Preview
@@ -832,6 +905,8 @@ fun PreviewChatItemView(
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
revealed = remember { mutableStateOf(false) },
range = 0..1,
selectedChatItems = remember { mutableStateOf(setOf()) },
selectChatItem = {},
deleteMessage = { _, _ -> },
deleteMessages = { _ -> },
receiveFile = { _ -> },
@@ -869,6 +944,8 @@ fun PreviewChatItemViewDeletedContent() {
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
revealed = remember { mutableStateOf(false) },
range = 0..1,
selectedChatItems = remember { mutableStateOf(setOf()) },
selectChatItem = {},
deleteMessage = { _, _ -> },
deleteMessages = { _ -> },
receiveFile = { _ -> },
@@ -27,6 +27,7 @@ import chat.simplex.common.views.chat.*
import chat.simplex.common.views.chat.group.deleteGroupDialog
import chat.simplex.common.views.chat.group.leaveGroupDialog
import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.contacts.onRequestAccepted
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.*
import chat.simplex.res.MR
@@ -34,7 +35,7 @@ import kotlinx.coroutines.*
import kotlinx.datetime.Clock
@Composable
fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, oneHandUI: State<Boolean>) {
fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
val showMenu = remember { mutableStateOf(false) }
val showMarkRead = remember(chat.chatStats.unreadCount, chat.chatStats.unreadChat) {
chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat
@@ -80,7 +81,6 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, oneHandUI:
disabled,
selectedChat,
nextChatSelected,
oneHandUI
)
}
is ChatInfo.Group ->
@@ -100,7 +100,6 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, oneHandUI:
disabled,
selectedChat,
nextChatSelected,
oneHandUI
)
is ChatInfo.Local -> {
ChatListNavLinkLayout(
@@ -119,7 +118,6 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, oneHandUI:
disabled,
selectedChat,
nextChatSelected,
oneHandUI
)
}
is ChatInfo.ContactRequest ->
@@ -129,7 +127,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, oneHandUI:
ContactRequestView(chat.chatInfo)
}
},
click = { contactRequestAlertDialog(chat.remoteHostId, chat.chatInfo, chatModel) },
click = { contactRequestAlertDialog(chat.remoteHostId, chat.chatInfo, chatModel) { onRequestAccepted(it) } },
dropdownMenuItems = {
tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) {
ContactRequestMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu)
@@ -139,7 +137,6 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, oneHandUI:
disabled,
selectedChat,
nextChatSelected,
oneHandUI
)
is ChatInfo.ContactConnection ->
ChatListNavLinkLayout(
@@ -160,7 +157,6 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, oneHandUI:
disabled,
selectedChat,
nextChatSelected,
oneHandUI
)
is ChatInfo.InvalidJSON ->
ChatListNavLinkLayout(
@@ -177,7 +173,6 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, oneHandUI:
disabled,
selectedChat,
nextChatSelected,
oneHandUI
)
}
}
@@ -878,7 +873,6 @@ expect fun ChatListNavLinkLayout(
disabled: Boolean,
selectedChat: State<Boolean>,
nextChatSelected: State<Boolean>,
oneHandUI: State<Boolean>
)
@Preview/*(
@@ -922,7 +916,6 @@ fun PreviewChatListNavLinkDirect() {
disabled = false,
selectedChat = remember { mutableStateOf(false) },
nextChatSelected = remember { mutableStateOf(false) },
oneHandUI = remember { mutableStateOf(false) }
)
}
}
@@ -968,7 +961,6 @@ fun PreviewChatListNavLinkGroup() {
disabled = false,
selectedChat = remember { mutableStateOf(false) },
nextChatSelected = remember { mutableStateOf(false) },
oneHandUI = remember { mutableStateOf(false) }
)
}
}
@@ -991,7 +983,6 @@ fun PreviewChatListNavLinkContactRequest() {
disabled = false,
selectedChat = remember { mutableStateOf(false) },
nextChatSelected = remember { mutableStateOf(false) },
oneHandUI = remember { mutableStateOf(false) }
)
}
}
@@ -12,7 +12,6 @@ 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.focus.*
import androidx.compose.ui.graphics.*
import androidx.compose.ui.text.font.FontStyle
@@ -25,6 +24,7 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.*
import chat.simplex.common.SettingsViewState
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
@@ -43,19 +43,28 @@ import kotlinx.serialization.json.Json
import java.net.URI
import kotlin.time.Duration.Companion.seconds
private fun showNewChatSheet(oneHandUI: State<Boolean>, barTitle: String) {
private fun showNewChatSheet(oneHandUI: State<Boolean>) {
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
ModalManager.start.showCustomModal { close ->
val close = {
// It will set it faster than in onDispose. It's important to catch the actual state before
// closing modal for reacting with status bar changes in [App]
chatModel.newChatSheetVisible.value = false
close()
}
ModalView(close, closeOnTop = !oneHandUI.value) {
if (appPlatform.isAndroid) {
BackHandler {
close()
}
}
NewChatSheet(rh = chatModel.currentRemoteHost.value, close)
DisposableEffect(Unit) {
onDispose {
chatModel.newChatSheetVisible.value = false
}
}
}
}
@@ -63,7 +72,7 @@ private fun showNewChatSheet(oneHandUI: State<Boolean>, barTitle: String) {
@Composable
fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
val oneHandUI = remember { chatModel.controller.appPrefs.oneHandUI }
val oneHandUI = remember { appPrefs.oneHandUI.state }
LaunchedEffect(Unit) {
if (shouldShowWhatsNew(chatModel)) {
@@ -87,25 +96,25 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
val (userPickerState, scaffoldState ) = settingsState
Scaffold(
topBar = {
if (!oneHandUI.state.value) {
Box(Modifier.padding(end = endPadding)) {
if (!oneHandUI.value) {
Column(Modifier.padding(end = endPadding)) {
ChatListToolbar(
scaffoldState.drawerState,
userPickerState,
stopped,
oneHandUI
)
Divider()
}
}
},
bottomBar = {
if (oneHandUI.state.value) {
Box(Modifier.padding(end = endPadding)) {
if (oneHandUI.value) {
Column(Modifier.padding(end = endPadding)) {
Divider()
ChatListToolbar(
scaffoldState.drawerState,
userPickerState,
stopped,
oneHandUI
)
}
}
@@ -121,11 +130,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 (!oneHandUI.state.value && searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
if (!oneHandUI.value && searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
FloatingActionButton(
onClick = {
if (!stopped) {
showNewChatSheet(oneHandUI.state, generalGetString(MR.strings.new_chat))
showNewChatSheet(oneHandUI)
}
},
Modifier
@@ -145,28 +154,17 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
}
}
) {
var modifier = Modifier.padding(it).padding(end = endPadding)
if (oneHandUI.state.value) {
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
}
Box(modifier) {
Box(Modifier.padding(it).padding(end = endPadding)) {
Box(
modifier = Modifier
.fillMaxSize()
) {
if (!chatModel.desktopNoUserNoRemote) {
ChatList(chatModel, searchText = searchText, oneHandUI = oneHandUI)
ChatList(chatModel, searchText = searchText)
}
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)
if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary)
}
}
}
@@ -184,7 +182,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
UserPicker(
chatModel = chatModel,
userPickerState = userPickerState,
contentAlignment = if (oneHandUI.state.value) Alignment.BottomStart else Alignment.TopStart
contentAlignment = if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart
) {
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
userPickerState.value = AnimatedViewState.GONE
@@ -210,32 +208,35 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
}
@Composable
private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean, oneHandUI: SharedPreference<Boolean>) {
private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean) {
val serversSummary: MutableState<PresentedServersSummary?> = remember { mutableStateOf(null) }
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
val updatingProgress = remember { chatModel.updatingProgress }.value
val oneHandUI = remember { appPrefs.oneHandUI.state }
if (oneHandUI.state.value) {
if (oneHandUI.value) {
val sp16 = with(LocalDensity.current) { 16.sp.toDp() }
barButtons.add {
IconButton(
onClick = {
if (!stopped) {
showNewChatSheet(oneHandUI.state, generalGetString(MR.strings.new_chat))
if (!stopped) {
barButtons.add {
IconButton(
onClick = {
showNewChatSheet(oneHandUI)
},
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(MaterialTheme.colors.primary, shape = CircleShape)
.size(33.dp * fontSizeSqrtMultiplier)
) {
Icon(
painterResource(MR.images.ic_edit_filled),
stringResource(MR.strings.add_contact_or_create_group),
Modifier.size(sp16),
tint = Color.White
)
}
},
) {
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)
}
}
}
@@ -327,7 +328,6 @@ private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableSt
onSearchValueChanged = {},
buttons = barButtons
)
Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier))
}
@Composable
@@ -440,14 +440,8 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
}
@Composable
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) {
private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, searchChatFilteredBySimplexLink: MutableState<String?>) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
val focusRequester = remember { FocusRequester() }
var focused by remember { mutableStateOf(false) }
Icon(
@@ -546,11 +540,13 @@ enum class ScrollDirection {
}
@Composable
private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldValue>, oneHandUI: SharedPreference<Boolean>) {
private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldValue>) {
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) }
val keyboardState by getKeyboardState()
val oneHandUI = remember { appPrefs.oneHandUI.state }
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
val currentIndex = listState.firstVisibleItemIndex
@@ -582,43 +578,47 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList())
LazyColumnWithScrollBar(
Modifier.fillMaxWidth(),
listState
Modifier.fillMaxSize(),
listState,
reverseLayout = oneHandUI.value
) {
stickyHeader {
Column(
Modifier
.offset {
val y = if (searchText.value.text.isEmpty()) {
if (oneHandUI.state.value && scrollDirection == ScrollDirection.Up) {
val offsetMultiplier = if (oneHandUI.value) 1 else -1
if (
(oneHandUI.value && scrollDirection == ScrollDirection.Up) ||
(appPlatform.isAndroid && keyboardState == KeyboardState.Opened)
) {
0
} else if (listState.firstVisibleItemIndex == 0) -listState.firstVisibleItemScrollOffset else -1000
} else if (listState.firstVisibleItemIndex == 0) offsetMultiplier * listState.firstVisibleItemScrollOffset else offsetMultiplier * 1000
} else {
0
}
IntOffset(0, y)
}
.background(MaterialTheme.colors.background)
) {
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink, oneHandUI)
Divider()
.background(MaterialTheme.colors.background),
) {
if (oneHandUI.value) {
Divider()
}
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
if (!oneHandUI.value) {
Divider()
}
}
}
itemsIndexed(chats, key = { _, chat -> chat.remoteHostId to chat.id }) { index, chat ->
val nextChatSelected = remember(chat.id, chats) { derivedStateOf {
chatModel.chatId.value != null && chats.getOrNull(index + 1)?.id == chatModel.chatId.value
} }
ChatListNavLinkView(chat, nextChatSelected, oneHandUI.state)
ChatListNavLinkView(chat, nextChatSelected)
}
}
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) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary)
}
}
@@ -341,10 +341,12 @@ fun ChatPreviewView(
val chat = activeVoicePreview.value?.chat ?: chat
val ci = activeVoicePreview.value?.ci ?: chat.chatItems.lastOrNull()
val mc = ci?.content?.msgContent
if ((showChatPreviews && chatModelDraftChatId != chat.id) || activeVoicePreview.value != null) {
val deleted = ci?.isDeletedContent == true || ci?.meta?.itemDeleted != null
val showContentPreview = (showChatPreviews && chatModelDraftChatId != chat.id && !deleted) || activeVoicePreview.value != null
if (ci != null && showContentPreview) {
chatItemContentPreview(chat, ci)
}
if (mc !is MsgContent.MCVoice || mc.text.isNotEmpty() || chatModelDraftChatId == chat.id) {
if (mc !is MsgContent.MCVoice || !showContentPreview || mc.text.isNotEmpty() || chatModelDraftChatId == chat.id) {
Box(Modifier.offset(x = if (mc is MsgContent.MCFile) -15.sp.toDp() else 0.dp)) {
chatPreviewText()
}
@@ -361,6 +363,13 @@ fun ChatPreviewView(
}
}
}
LaunchedEffect(chatModel.deletedChats.value) {
val voicePreview = activeVoicePreview.value
// Stop voice when deleting the chat
if (chatModel.deletedChats.value.contains(chatModel.remoteHostId() to chat.id) && voicePreview?.ci != null) {
AudioPlayer.stop(voicePreview.ci)
}
}
}
Spacer(Modifier.width(8.sp.toDp()))
@@ -68,7 +68,6 @@ enum class SubscriptionColorType {
data class SubscriptionStatus(
val color: SubscriptionColorType,
val variableValue: Float,
val opacity: Float,
val statusPercent: Float
)
@@ -78,7 +77,6 @@ fun subscriptionStatusColorAndPercentage(
subs: SMPServerSubs,
hasSess: Boolean
): SubscriptionStatus {
fun roundedToQuarter(n: Float): Float = when {
n >= 1 -> 1f
n <= 0 -> 0f
@@ -86,23 +84,26 @@ fun subscriptionStatusColorAndPercentage(
}
val activeColor: SubscriptionColorType = if (socksProxy != null) SubscriptionColorType.ACTIVE_SOCKS_PROXY else SubscriptionColorType.ACTIVE
val noConnColorAndPercent = SubscriptionStatus(SubscriptionColorType.DISCONNECTED, 1f, 1f, 0f)
val noConnColorAndPercent = SubscriptionStatus(SubscriptionColorType.DISCONNECTED, 1f, 0f)
val activeSubsRounded = roundedToQuarter(subs.shareOfActive)
return if (online && subs.total > 0) {
if (subs.ssActive == 0) {
if (hasSess)
SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive)
else
noConnColorAndPercent
} else { // ssActive > 0
if (hasSess)
SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive)
else
// This would mean implementation error
SubscriptionStatus(SubscriptionColorType.ACTIVE_DISCONNECTED, activeSubsRounded, subs.shareOfActive, subs.shareOfActive)
}
} else noConnColorAndPercent
return if (!online)
noConnColorAndPercent
else if (subs.total == 0 && !hasSess)
// On freshly installed app (without chats) and on app start
SubscriptionStatus(activeColor, 0f, 0f)
else if (subs.ssActive == 0) {
if (hasSess)
SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive)
else
noConnColorAndPercent
} else { // ssActive > 0
if (hasSess)
SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive)
else
// This would mean implementation error
SubscriptionStatus(SubscriptionColorType.ACTIVE_DISCONNECTED, activeSubsRounded, subs.shareOfActive)
}
}
@Composable
@@ -6,7 +6,6 @@ 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
@@ -22,8 +21,7 @@ fun ShareListNavLinkView(
chatModel: ChatModel,
isMediaOrFileAttachment: Boolean,
isVoice: Boolean,
hasSimplexLink: Boolean,
oneHandUI: State<Boolean>
hasSimplexLink: Boolean
) {
val stopped = chatModel.chatRunning.value == false
val scope = rememberCoroutineScope()
@@ -31,7 +29,7 @@ fun ShareListNavLinkView(
is ChatInfo.Direct -> {
val voiceProhibited = isVoice && !chat.chatInfo.featureEnabled(ChatFeature.Voice)
ShareListNavLinkLayout(
chatLinkPreview = { SharePreviewView(chat, disabled = voiceProhibited, oneHandUI = oneHandUI) },
chatLinkPreview = { SharePreviewView(chat, disabled = voiceProhibited) },
click = {
if (voiceProhibited) {
showForwardProhibitedByPrefAlert()
@@ -48,7 +46,7 @@ fun ShareListNavLinkView(
val voiceProhibited = isVoice && !chat.chatInfo.featureEnabled(ChatFeature.Voice)
val prohibitedByPref = simplexLinkProhibited || fileProhibited || voiceProhibited
ShareListNavLinkLayout(
chatLinkPreview = { SharePreviewView(chat, disabled = prohibitedByPref, oneHandUI = oneHandUI) },
chatLinkPreview = { SharePreviewView(chat, disabled = prohibitedByPref) },
click = {
if (prohibitedByPref) {
showForwardProhibitedByPrefAlert()
@@ -61,7 +59,7 @@ fun ShareListNavLinkView(
}
is ChatInfo.Local ->
ShareListNavLinkLayout(
chatLinkPreview = { SharePreviewView(chat, disabled = false, oneHandUI = oneHandUI) },
chatLinkPreview = { SharePreviewView(chat, disabled = false) },
click = { scope.launch { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) } },
stopped
)
@@ -89,15 +87,9 @@ private fun ShareListNavLinkLayout(
}
@Composable
private fun SharePreviewView(chat: Chat, disabled: Boolean, oneHandUI: State<Boolean>) {
var modifier = Modifier.fillMaxSize()
if (oneHandUI.value) {
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
}
private fun SharePreviewView(chat: Chat, disabled: Boolean) {
Row(
modifier,
Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@@ -7,7 +7,6 @@ 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
@@ -15,6 +14,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import chat.simplex.common.SettingsViewState
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.views.helpers.*
import chat.simplex.common.platform.*
import chat.simplex.res.MR
@@ -25,16 +25,30 @@ 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 }
val oneHandUI = remember { appPrefs.oneHandUI.state }
Scaffold(
Modifier.padding(end = endPadding),
contentColor = LocalContentColor.current,
drawerContentColor = LocalContentColor.current,
scaffoldState = scaffoldState,
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() } } },
) {
topBar = {
if (!oneHandUI.value) {
Column {
ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() }
Divider()
}
}
},
bottomBar = {
if (oneHandUI.value) {
Column {
Divider()
ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() }
}
}
}
) {
val sharedContent = chatModel.sharedContent.value
var isMediaOrFileAttachment = false
var isVoice = false
@@ -60,15 +74,9 @@ 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
modifier = Modifier.fillMaxSize()
) {
if (chatModel.chats.value.isNotEmpty()) {
ShareList(
@@ -77,10 +85,9 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
isMediaOrFileAttachment = isMediaOrFileAttachment,
isVoice = isVoice,
hasSimplexLink = hasSimplexLink,
oneHandUI = oneHandUI.state
)
} else {
EmptyList(oneHandUI = oneHandUI.state)
EmptyList()
}
}
}
@@ -101,14 +108,8 @@ private fun hasSimplexLink(msg: String): Boolean {
}
@Composable
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) {
private fun EmptyList() {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(stringResource(MR.strings.you_have_no_chats), color = MaterialTheme.colors.secondary)
}
}
@@ -189,7 +190,6 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
onSearchValueChanged = onSearchValueChanged,
buttons = barButtons
)
Divider()
}
@Composable
@@ -199,8 +199,8 @@ private fun ShareList(
isMediaOrFileAttachment: Boolean,
isVoice: Boolean,
hasSimplexLink: Boolean,
oneHandUI: State<Boolean>
) {
val oneHandUI = remember { appPrefs.oneHandUI.state }
val chats by remember(search) {
derivedStateOf {
val sorted = chatModel.chats.value.toList().sortedByDescending { it.chatInfo is ChatInfo.Local }
@@ -212,7 +212,8 @@ private fun ShareList(
}
}
LazyColumnWithScrollBar(
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxSize(),
reverseLayout = oneHandUI.value
) {
items(chats) { chat ->
ShareListNavLinkView(
@@ -221,7 +222,6 @@ private fun ShareList(
isMediaOrFileAttachment = isMediaOrFileAttachment,
isVoice = isVoice,
hasSimplexLink = hasSimplexLink,
oneHandUI = oneHandUI
)
}
}
@@ -21,6 +21,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.ui.theme.*
@@ -149,7 +150,7 @@ fun UserPicker(
.padding(bottom = 10.dp, top = 10.dp)
.graphicsLayer {
alpha = animatedFloat.value
translationY = (animatedFloat.value - 1) * xOffset
translationY = (if (appPrefs.oneHandUI.state.value) -1 else 1) * (animatedFloat.value - 1) * xOffset
},
contentAlignment = contentAlignment
) {
@@ -16,7 +16,7 @@ import chat.simplex.common.views.newchat.chatContactType
import chat.simplex.res.MR
import kotlinx.coroutines.delay
private fun onRequestAccepted(chat: Chat) {
fun onRequestAccepted(chat: Chat) {
val chatInfo = chat.chatInfo
if (chatInfo is ChatInfo.Direct) {
ModalManager.start.closeModals()
@@ -27,7 +27,7 @@ private fun onRequestAccepted(chat: Chat) {
}
@Composable
fun ContactListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, oneHandUI: State<Boolean>) {
fun ContactListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, showDeletedChatIcon: 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)
@@ -46,7 +46,7 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, oneHand
ChatListNavLinkLayout(
chatLinkPreview = {
tryOrShowError("${chat.id}ContactListNavLink", error = { ErrorChatListItem() }) {
ContactPreviewView(chat, disabled)
ContactPreviewView(chat, disabled, showDeletedChatIcon)
}
},
click = {
@@ -88,14 +88,13 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, oneHand
disabled,
selectedChat,
nextChatSelected,
oneHandUI
)
}
is ChatInfo.ContactRequest -> {
ChatListNavLinkLayout(
chatLinkPreview = {
tryOrShowError("${chat.id}ContactListNavLink", error = { ErrorChatListItem() }) {
ContactPreviewView(chat, disabled)
ContactPreviewView(chat, disabled, showDeletedChatIcon)
}
},
click = {
@@ -121,9 +120,7 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, oneHand
showMenu,
disabled,
selectedChat,
nextChatSelected,
oneHandUI
)
nextChatSelected)
}
else -> {}
}
@@ -21,6 +21,7 @@ import chat.simplex.res.MR
fun ContactPreviewView(
chat: Chat,
disabled: Boolean,
showDeletedChatIcon: Boolean
) {
val cInfo = chat.chatInfo
val contactType = chatContactType(chat)
@@ -104,10 +105,21 @@ fun ContactPreviewView(
)
}
if (chat.chatInfo.chatSettings?.favorite == true) {
if (showDeletedChatIcon && chat.chatInfo.chatDeleted) {
Icon(
painterResource(MR.images.ic_inventory_2),
contentDescription = null,
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.size(17.dp)
)
if (chat.chatInfo.incognito) {
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON))
}
} else if (chat.chatInfo.chatSettings?.favorite == true) {
Icon(
painterResource(MR.images.ic_star_filled),
contentDescription = generalGetString(MR.strings.favorite_chat),
contentDescription = null,
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.size(17.dp)
@@ -522,9 +522,17 @@ private fun exportArchive(
progressIndicator.value = true
withLongRunningApi {
try {
val archiveFile = exportChatArchive(m, null, chatArchiveName, chatArchiveTime, chatArchiveFile)
val (archiveFile, archiveErrors) = exportChatArchive(m, null, chatArchiveName, chatArchiveTime, chatArchiveFile)
chatArchiveFile.value = archiveFile
saveArchiveLauncher.launch(archiveFile.substringAfterLast(File.separator))
if (archiveErrors.isEmpty()) {
saveArchiveLauncher.launch(archiveFile.substringAfterLast(File.separator))
} else {
showArchiveExportedWithErrorsAlert(generalGetString(MR.strings.chat_database_exported_save), archiveErrors) {
withLongRunningApi {
saveArchiveLauncher.launch(archiveFile.substringAfterLast(File.separator))
}
}
}
progressIndicator.value = false
} catch (e: Error) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_exporting_chat_database), e.toString())
@@ -539,7 +547,7 @@ suspend fun exportChatArchive(
chatArchiveName: MutableState<String?>,
chatArchiveTime: MutableState<Instant?>,
chatArchiveFile: MutableState<String?>
): String {
): Pair<String, List<ArchiveError>> {
val archiveTime = Clock.System.now()
val ts = SimpleDateFormat("yyyy-MM-dd'T'HHmmss", Locale.US).format(Date.from(archiveTime.toJavaInstant()))
val archiveName = "simplex-chat.$ts.zip"
@@ -550,7 +558,7 @@ suspend fun exportChatArchive(
controller.apiSaveAppSettings(AppSettings.current.prepareForExport())
}
wallpapersDir.mkdirs()
m.controller.apiExportArchive(config)
val archiveErrors = m.controller.apiExportArchive(config)
if (storagePath == null) {
deleteOldArchive(m)
m.controller.appPrefs.chatArchiveName.set(archiveName)
@@ -559,7 +567,7 @@ suspend fun exportChatArchive(
chatArchiveName.value = archiveName
chatArchiveTime.value = archiveTime
chatArchiveFile.value = archivePath
return archivePath
return archivePath to archiveErrors
}
private fun deleteOldArchive(m: ChatModel) {
@@ -592,6 +600,28 @@ private fun importArchiveAlert(
)
}
fun showArchiveImportedWithErrorsAlert(archiveErrors: List<ArchiveError>) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.chat_database_imported),
text = generalGetString(MR.strings.restart_the_app_to_use_imported_chat_database) + "\n\n" + generalGetString(MR.strings.non_fatal_errors_occured_during_import) + archiveErrorsText(archiveErrors))
}
fun showArchiveExportedWithErrorsAlert(description: String, archiveErrors: List<ArchiveError>, onConfirm: () -> Unit) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.chat_database_exported_title),
text = description + "\n\n" + generalGetString(MR.strings.chat_database_exported_not_all_files) + archiveErrorsText(archiveErrors),
confirmText = generalGetString(MR.strings.chat_database_exported_continue),
onConfirm = onConfirm
)
}
private fun archiveErrorsText(errs: List<ArchiveError>): String = "\n" + errs.map {
when (it) {
is ArchiveError.ArchiveErrorImport -> it.importError
is ArchiveError.ArchiveErrorFile -> "${it.file}: ${it.fileError}"
}
}.joinToString(separator = "\n")
private fun importArchive(
m: ChatModel,
importedArchiveURI: URI,
@@ -621,7 +651,7 @@ private fun importArchive(
}
} else {
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.chat_database_imported), text = generalGetString(MR.strings.restart_the_app_to_use_imported_chat_database) + "\n" + generalGetString(MR.strings.non_fatal_errors_occured_during_import))
showArchiveImportedWithErrorsAlert(archiveErrors)
}
}
} catch (e: Error) {
@@ -11,8 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.draw.*
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.*
import androidx.compose.ui.layout.ContentScale
@@ -55,7 +54,8 @@ fun ProfileImage(
image: String? = null,
icon: ImageResource = MR.images.ic_account_circle_filled,
color: Color = MaterialTheme.colors.secondaryVariant,
backgroundColor: Color? = null
backgroundColor: Color? = null,
blurred: Boolean = false
) {
Box(Modifier.size(size)) {
if (image == null) {
@@ -88,7 +88,7 @@ fun ProfileImage(
imageBitmap,
stringResource(MR.strings.image_descr_profile_image),
contentScale = ContentScale.Crop,
modifier = ProfileIconModifier(size)
modifier = ProfileIconModifier(size, blurred = blurred)
)
}
}
@@ -109,12 +109,12 @@ private const val squareToCircleRatio = 0.935f
private const val radiusFactor = (1 - squareToCircleRatio) / 50
@Composable
fun ProfileIconModifier(size: Dp, padding: Boolean = true): Modifier {
fun ProfileIconModifier(size: Dp, padding: Boolean = true, blurred: Boolean = false): Modifier {
val percent = remember { appPreferences.profileImageCornerRadius.state }
val r = max(0f, percent.value)
val pad = if (padding) size / 12 else 0.dp
val m = Modifier.size(size)
return when {
val m1 = when {
r >= 50 ->
m.padding(pad).clip(CircleShape)
r <= 0 -> {
@@ -126,6 +126,7 @@ fun ProfileIconModifier(size: Dp, padding: Boolean = true): Modifier {
m.padding((size - sz) / 2).clip(RoundedCornerShape(size = sz * r / 100))
}
}
return if (blurred) m1.blur(size / 4) else m1
}
/** [AccountCircleFilled] has its inner padding which leads to visible border if there is background underneath.
@@ -20,7 +20,7 @@ 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, arrangement: Arrangement.Vertical = Arrangement.Top, closeBarTitle: String? = null, 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, barPaddingValues: PaddingValues = PaddingValues(horizontal = AppBarHorizontalPadding), endButtons: @Composable RowScope.() -> Unit = {}) {
var rowModifier = Modifier
.fillMaxWidth()
.height(AppBarHeight * fontSizeSqrtMultiplier)
@@ -36,7 +36,7 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Co
.heightIn(min = AppBarHeight * fontSizeSqrtMultiplier)
) {
Row(
modifier = Modifier.padding(horizontal = AppBarHorizontalPadding),
modifier = Modifier.padding(barPaddingValues),
content = {
Row(
rowModifier,
@@ -70,7 +70,7 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Co
}
@Composable
fun AppBarTitle(title: String, hostDevice: Pair<Long?, String>? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f) {
fun AppBarTitle(title: String, hostDevice: Pair<Long?, String>? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f + 8.dp) {
val theme = CurrentColors.collectAsState()
val titleColor = MaterialTheme.appColors.title
val brush = if (theme.value.base == DefaultTheme.SIMPLEX)
@@ -53,6 +53,15 @@ fun NavigationButtonBack(onButtonClicked: (() -> Unit)?, tintColor: Color = if (
}
}
@Composable
fun NavigationButtonClose(onButtonClicked: (() -> Unit)?, tintColor: Color = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, height: Dp = 24.dp) {
IconButton(onButtonClicked ?: {}, enabled = onButtonClicked != null) {
Icon(
painterResource(MR.images.ic_close), stringResource(MR.strings.back), Modifier.height(height), tint = tintColor
)
}
}
@Composable
fun ShareButton(onButtonClicked: () -> Unit) {
IconButton(onButtonClicked) {
@@ -26,7 +26,6 @@ fun ModalView(
background: Color = MaterialTheme.colors.background,
modifier: Modifier = Modifier,
closeOnTop: Boolean = true,
closeBarTitle: String? = null,
endButtons: @Composable RowScope.() -> Unit = {},
content: @Composable () -> Unit,
) {
@@ -38,14 +37,10 @@ fun ModalView(
if (closeOnTop) {
CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons)
}
Box(if (closeOnTop) modifier else modifier.padding(bottom = AppBarHeight * fontSizeSqrtMultiplier)) {
Box(modifier = modifier) {
content()
}
}
if (!closeOnTop) {
CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons, arrangement = Arrangement.Bottom, closeBarTitle = closeBarTitle)
}
}
}
@@ -72,17 +67,17 @@ class ModalManager(private val placement: ModalPlacement? = null) {
// 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, closeOnTop: Boolean = true, closeBarTitle: String? = null,endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
fun showModal(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
val data = ModalData()
showCustomModal { close ->
ModalView(close, showClose = showClose, closeOnTop = closeOnTop, closeBarTitle = closeBarTitle, endButtons = endButtons, content = { data.content() })
ModalView(close, showClose = showClose, closeOnTop = closeOnTop, endButtons = endButtons, content = { data.content() })
}
}
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, closeBarTitle: String? = null, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
val data = ModalData()
showCustomModal { close ->
ModalView(close, showClose = showClose, endButtons = endButtons, closeOnTop = closeOnTop, closeBarTitle = closeBarTitle, content = { data.content(close) })
ModalView(close, showClose = showClose, endButtons = endButtons, closeOnTop = closeOnTop, content = { data.content(close) })
}
}
@@ -63,6 +63,7 @@ fun SearchTextField(
focusedIndicatorColor = Color.Unspecified,
unfocusedIndicatorColor = Color.Unspecified,
disabledIndicatorColor = Color.Unspecified,
placeholderColor = MaterialTheme.colors.secondary,
)
val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize)
val interactionSource = remember { MutableInteractionSource() }
@@ -88,7 +89,7 @@ fun SearchTextField(
textStyle = TextStyle(
color = MaterialTheme.colors.onBackground,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
fontSize = 15.sp
),
interactionSource = interactionSource,
decorationBox = @Composable { innerTextField ->
@@ -480,12 +480,13 @@ private fun MutableState<MigrationFromState>.exportArchive() {
withLongRunningApi {
try {
getMigrationTempFilesDirectory().mkdir()
val archivePath = exportChatArchive(chatModel, getMigrationTempFilesDirectory(), mutableStateOf(""), mutableStateOf(Instant.DISTANT_PAST), mutableStateOf(""))
val totalBytes = File(archivePath).length()
if (totalBytes > 0L) {
state = MigrationFromState.DatabaseInit(totalBytes, archivePath)
val (archivePath, archiveErrors) = exportChatArchive(chatModel, getMigrationTempFilesDirectory(), mutableStateOf(""), mutableStateOf(Instant.DISTANT_PAST), mutableStateOf(""))
if (archiveErrors.isEmpty()) {
uploadArchive(archivePath)
} else {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_from_device_exported_file_doesnt_exist))
showArchiveExportedWithErrorsAlert(generalGetString(MR.strings.chat_database_exported_migrate), archiveErrors) {
uploadArchive(archivePath)
}
state = MigrationFromState.UploadConfirmation
}
} catch (e: Exception) {
@@ -498,6 +499,17 @@ private fun MutableState<MigrationFromState>.exportArchive() {
}
}
private fun MutableState<MigrationFromState>.uploadArchive(archivePath: String) {
val totalBytes = File(archivePath).length()
if (totalBytes > 0L) {
state = MigrationFromState.DatabaseInit(totalBytes, archivePath)
} else {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_from_device_exported_file_doesnt_exist))
state = MigrationFromState.UploadConfirmation
}
}
suspend fun initTemporaryDatabase(tempDatabaseFile: File, netCfg: NetCfg): Pair<ChatCtrl, User>? {
val (status, ctrl) = chatInitTemporaryDatabase(tempDatabaseFile.absolutePath)
showErrorOnMigrationIfNeeded(status)
@@ -237,7 +237,6 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
proxy
}
}
val proxyPort = remember { derivedStateOf { networkProxyHostPort.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } }
val netCfg = rememberSaveable(stateSaver = serializableSaver()) {
mutableStateOf(getNetCfg().withOnionHosts(onionHosts.value).copy(socksProxy = socksProxy, sessionMode = sessionMode.value))
@@ -275,7 +274,6 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
onionHosts,
sessionMode,
networkProxyHostPortPref,
proxyPort,
toggleSocksProxy = { enable ->
networkUseSocksProxy.value = enable
},
@@ -599,10 +597,7 @@ private fun MutableState<MigrationToState?>.importArchive(archivePath: String, n
val config = ArchiveConfig(archivePath, parentTempDirectory = databaseExportDir.toString())
val archiveErrors = controller.apiImportArchive(config)
if (archiveErrors.isNotEmpty()) {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.chat_database_imported),
generalGetString(MR.strings.non_fatal_errors_occured_during_import)
)
showArchiveImportedWithErrorsAlert(archiveErrors)
}
state = MigrationToState.Passphrase("", netCfg)
MigrationToDeviceState.save(MigrationToDeviceState.Passphrase(netCfg))
@@ -14,7 +14,6 @@ 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.focus.*
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.Painter
@@ -28,6 +27,7 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chatlist.ScrollDirection
@@ -39,45 +39,47 @@ import java.net.URI
@Composable
fun NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) {
val oneHandUI = remember { chatModel.controller.appPrefs.oneHandUI }
val oneHandUI = remember { appPrefs.oneHandUI.state }
val keyboardState by getKeyboardState()
val showToolbarInOneHandUI = remember { derivedStateOf { keyboardState == KeyboardState.Closed && oneHandUI.value } }
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
)
Scaffold(
bottomBar = {
if (showToolbarInOneHandUI.value) {
Column {
Divider()
CloseSheetBar(
close = close,
showClose = true,
endButtons = { Spacer(Modifier.minimumInteractiveComponentSize()) },
arrangement = Arrangement.Bottom,
closeBarTitle = generalGetString(MR.strings.new_chat),
barPaddingValues = PaddingValues(horizontal = 0.dp)
)
}
}
}
) {
Column(
modifier = Modifier.fillMaxSize().padding(it)
) {
val closeAll = { ModalManager.start.closeModals() }
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
)
Column(modifier = Modifier.fillMaxSize()) {
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
)
}
}
}
}
@@ -90,7 +92,7 @@ fun chatContactType(chat: Chat): ContactType {
return when (val cInfo = chat.chatInfo) {
is ChatInfo.ContactRequest -> ContactType.REQUEST
is ChatInfo.Direct -> {
val contact = cInfo.contact;
val contact = cInfo.contact
when {
contact.activeConn == null && contact.profile.contactLink != null -> ContactType.CARD
@@ -116,15 +118,15 @@ private fun NewChatSheetLayout(
scanPaste: () -> Unit,
createGroup: () -> Unit,
close: () -> Unit,
oneHandUI: SharedPreference<Boolean>
) {
val oneHandUI = remember { appPrefs.oneHandUI.state }
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()) {
val baseContactTypes = remember { listOf(ContactType.CARD, ContactType.RECENT, ContactType.REQUEST) }
val contactTypes by remember(searchText.value.text.isEmpty()) {
derivedStateOf { contactTypesSearchTargets(baseContactTypes, searchText.value.text.isEmpty()) }
}
val allChats by remember(chatModel.chats.value, contactTypes) {
@@ -133,6 +135,7 @@ private fun NewChatSheetLayout(
var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) }
var previousIndex by remember { mutableStateOf(0) }
var previousScrollOffset by remember { mutableStateOf(0) }
val keyboardState by getKeyboardState()
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
val currentIndex = listState.firstVisibleItemIndex
@@ -160,24 +163,38 @@ private fun NewChatSheetLayout(
contactChats = allChats
)
var sectionModifier = Modifier.fillMaxWidth()
if (oneHandUI.state.value) {
sectionModifier = sectionModifier.scale(scaleX = 1f, scaleY = -1f)
}
val sectionModifier = Modifier.fillMaxWidth()
LazyColumnWithScrollBar(
Modifier.fillMaxWidth(),
listState
Modifier.fillMaxSize(),
listState,
reverseLayout = oneHandUI.value
) {
if (!oneHandUI.value) {
item {
Box(contentAlignment = Alignment.Center) {
val bottomPadding = DEFAULT_PADDING
AppBarTitle(
stringResource(MR.strings.new_chat),
hostDevice(rh?.remoteHostId),
bottomPadding = bottomPadding
)
}
}
}
stickyHeader {
Column(
Modifier
.offset {
val y = if (searchText.value.text.isEmpty()) {
if (oneHandUI.state.value && scrollDirection == ScrollDirection.Up) {
val offsetMultiplier = if (oneHandUI.value) 1 else -1
if (
(oneHandUI.value && scrollDirection == ScrollDirection.Up) ||
(appPlatform.isAndroid && keyboardState == KeyboardState.Opened)
) {
0
} else if (listState.firstVisibleItemIndex == 0) -listState.firstVisibleItemScrollOffset else -1000
} else if (listState.firstVisibleItemIndex == 0) offsetMultiplier * listState.firstVisibleItemScrollOffset else offsetMultiplier * 1000
} else {
0
}
@@ -185,47 +202,57 @@ private fun NewChatSheetLayout(
}
.background(MaterialTheme.colors.background)
) {
if (!oneHandUI.state.value) {
Divider()
}
Divider()
ContactsSearchBar(
listState = listState,
searchText = searchText,
searchShowingSimplexLink = searchShowingSimplexLink,
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
close = close,
oneHandUI = oneHandUI
)
Divider()
if (!oneHandUI.value) {
Divider()
}
}
}
item {
Spacer(Modifier.padding(bottom = DEFAULT_PADDING))
Spacer(Modifier.padding(bottom = 27.dp))
val actionButtonsOriginal = listOf(
Triple(
painterResource(MR.images.ic_add_link),
stringResource(MR.strings.add_contact_tab),
addContact,
),
Triple(
painterResource(MR.images.ic_qr_code),
if (appPlatform.isAndroid) stringResource(MR.strings.scan_paste_link) else stringResource(MR.strings.paste_link),
scanPaste,
),
Triple(
painterResource(MR.images.ic_group),
stringResource(MR.strings.create_group_button),
createGroup,
)
)
val actionButtons by remember(oneHandUI.value) {
derivedStateOf {
if (oneHandUI.value) actionButtonsOriginal.asReversed() else actionButtonsOriginal
}
}
if (searchText.value.text.isEmpty()) {
Row {
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
)
actionButtons.map {
NewChatButton(
icon = it.first,
text = it.second,
click = it.third,
extraPadding = true,
)
}
}
}
SectionDividerSpaced(maxBottomPadding = false)
@@ -242,11 +269,9 @@ private fun NewChatSheetLayout(
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()) }
closeOnTop = !oneHandUI.value,
) {
DeletedContactsView(rh = rh, close = {
DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = {
ModalManager.start.closeModals()
})
}
@@ -254,7 +279,7 @@ private fun NewChatSheetLayout(
}
) {
Icon(
painterResource(MR.images.ic_folder_open),
painterResource(MR.images.ic_inventory_2),
contentDescription = stringResource(MR.strings.deleted_chats),
tint = MaterialTheme.colors.secondary,
)
@@ -263,16 +288,16 @@ private fun NewChatSheetLayout(
}
}
}
SectionDividerSpaced()
SectionDividerSpaced(maxBottomPadding = false)
}
}
}
item {
if (filteredContactChats.isNotEmpty() && !oneHandUI.state.value) {
if (filteredContactChats.isNotEmpty() && !oneHandUI.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
modifier = sectionModifier.padding(start = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF, bottom = DEFAULT_PADDING_HALF), fontSize = 12.sp
)
}
}
@@ -283,7 +308,7 @@ private fun NewChatSheetLayout(
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
}
}
ContactListNavLinkView(chat, nextChatSelected, oneHandUI.state)
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true)
}
}
@@ -308,10 +333,9 @@ private fun NewChatButton(
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) {
Row {
Icon(icon, text, tint = if (disabled) MaterialTheme.colors.secondary else iconColor)
TextIconSpaced(extraPadding)
Text(text, color = if (disabled) MaterialTheme.colors.secondary else textColor)
@@ -326,17 +350,10 @@ private fun ContactsSearchBar(
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) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
val focusRequester = remember { FocusRequester() }
Icon(
painterResource(MR.images.ic_search),
@@ -416,7 +433,7 @@ private fun ContactsSearchBar(
@Composable
private fun ToggleFilterButton() {
val pref = remember { ChatController.appPrefs.showUnreadAndFavorites }
val pref = remember { appPrefs.showUnreadAndFavorites }
IconButton(onClick = { pref.set(!pref.get()) }) {
val sp16 = with(LocalDensity.current) { 16.sp.toDp() }
Icon(
@@ -471,7 +488,7 @@ private fun filteredContactChats(
}
private fun filterChat(chat: Chat, searchText: String, showUnreadAndFavorites: Boolean): Boolean {
var meetsPredicate = true;
var meetsPredicate = true
val s = searchText.trim().lowercase()
val cInfo = chat.chatInfo
@@ -485,7 +502,7 @@ private fun filterChat(chat: Chat, searchText: String, showUnreadAndFavorites: B
meetsPredicate = meetsPredicate && (cInfo.chatSettings?.favorite ?: false)
}
return meetsPredicate;
return meetsPredicate
}
private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean =
@@ -512,85 +529,100 @@ private fun contactTypesSearchTargets(baseContactTypes: List<ContactType>, searc
}
@Composable
private fun DeletedContactsView(rh: RemoteHostInfo?, close: () -> Unit) {
val oneHandUI = remember { chatModel.controller.appPrefs.oneHandUI }
private fun DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats: () -> Unit, close: () -> Unit) {
val oneHandUI = remember { appPrefs.oneHandUI.state }
val keyboardState by getKeyboardState()
val showToolbarInOneHandUI = remember { derivedStateOf { keyboardState == KeyboardState.Closed && oneHandUI.value } }
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
Scaffold(
bottomBar = {
if (showToolbarInOneHandUI.value) {
Column {
Divider()
CloseSheetBar(
close = closeDeletedChats,
showClose = true,
endButtons = { Spacer(Modifier.minimumInteractiveComponentSize()) },
arrangement = Arrangement.Bottom,
closeBarTitle = generalGetString(MR.strings.deleted_chats),
barPaddingValues = PaddingValues(horizontal = 0.dp)
)
}
}
}
) {
Column(
Modifier
.fillMaxSize()
.padding(it)
) {
if (!oneHandUI.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 { appPrefs.showUnreadAndFavorites.state }.value
val allChats by remember(chatModel.chats.value) {
derivedStateOf { filterContactTypes(chatModel.chats.value, listOf(ContactType.CHAT_DELETED)) }
}
val filteredContactChats = filteredContactChats(
showUnreadAndFavorites = showUnreadAndFavorites,
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
searchShowingSimplexLink = searchShowingSimplexLink,
searchText = searchText.value.text,
contactChats = allChats
)
LazyColumnWithScrollBar(
Modifier.fillMaxSize(),
reverseLayout = oneHandUI.value,
) {
item {
if (!oneHandUI.value) {
Divider()
}
ContactsSearchBar(
listState = listState,
searchText = searchText,
searchShowingSimplexLink = searchShowingSimplexLink,
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
close = close,
)
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, showDeletedChatIcon = false)
}
}
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,
)
}
}
}
}
}
}
@@ -61,9 +61,8 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC
/** When [AddContactLearnMore] is open, we don't need to drop [ChatModel.showingInvitation].
* Otherwise, it will be called here AFTER [AddContactLearnMore] is launched and will clear the value too soon.
* 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.start.openModalCount() == 1 || appPlatform.isDesktop)) {
if (chatModel.showingInvitation.value != null && ModalManager.start.openModalCount() == 1) {
val conn = contactConnection.value
if (chatModel.showingInvitation.value?.connChatUsed == false && conn != null) {
AlertManager.shared.showAlertDialog(
@@ -78,7 +77,7 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC
controller.deleteChat(Chat(remoteHostId = rh?.remoteHostId, chatInfo = chatInfo, chatItems = listOf()))
if (chatModel.chatId.value == chatInfo.id) {
chatModel.chatId.value = null
ModalManager.end.closeModals()
ModalManager.start.closeModals()
}
}
}
@@ -212,8 +211,7 @@ private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection
Spacer(Modifier.height(10.dp))
val incognito = remember { mutableStateOf(controller.appPrefs.incognito.get()) }
IncognitoToggle(controller.appPrefs.incognito, incognito) {
if (appPlatform.isDesktop) ModalManager.end.closeModals()
ModalManager.end.showModal { IncognitoView() }
ModalManager.start.showModal { IncognitoView() }
}
KeyChangeEffect(incognito.value) {
withBGApi {
@@ -233,8 +231,7 @@ private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection
private fun AddContactLearnMoreButton() {
IconButton(
{
if (appPlatform.isDesktop) ModalManager.end.closeModals()
ModalManager.end.showModalCloseable { close ->
ModalManager.start.showModalCloseable { close ->
AddContactLearnMore(close)
}
},
@@ -18,7 +18,7 @@ import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
@@ -115,7 +115,9 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
AppBarTitle(String.format(generalGetString(MR.strings.new_in_version), v.version), bottomPadding = DEFAULT_PADDING)
v.features.forEach { feature ->
featureDescription(painterResource(feature.icon), feature.titleId, feature.descrId, feature.link)
if (feature.show) {
featureDescription(painterResource(feature.icon), feature.titleId, feature.descrId, feature.link)
}
}
if (v.post != null) {
@@ -158,7 +160,8 @@ private data class FeatureDescription(
val icon: ImageResource,
val titleId: StringResource,
val descrId: StringResource,
val link: String? = null
val link: String? = null,
val show: Boolean = true
)
private data class VersionDescription(
@@ -587,6 +590,43 @@ private val versionDescriptions: List<VersionDescription> = listOf(
)
)
),
VersionDescription(
version = "v6.0",
post = "https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html",
features = listOf(
FeatureDescription(
icon = MR.images.ic_settings_ethernet,
titleId = MR.strings.v5_8_private_routing,
descrId = MR.strings.v6_0_private_routing_descr
),
FeatureDescription(
icon = MR.images.ic_id_card,
titleId = MR.strings.v6_0_your_contacts,
descrId = MR.strings.v6_0_your_contacts_descr
),
FeatureDescription(
icon = MR.images.ic_toast,
titleId = MR.strings.v6_0_reachable_chat_toolbar,
descrId = MR.strings.v6_0_reachable_chat_toolbar_descr,
show = appPlatform.isAndroid
),
FeatureDescription(
icon = MR.images.ic_link,
titleId = MR.strings.v6_0_connect_faster,
descrId = MR.strings.v6_0_connect_faster_descr
),
FeatureDescription(
icon = MR.images.ic_delete,
titleId = MR.strings.v6_0_delete_many_messages,
descrId = MR.strings.v6_0_delete_many_messages_descr
),
FeatureDescription(
icon = MR.images.ic_wifi_tethering,
titleId = MR.strings.v6_0_connection_servers_status,
descrId = MR.strings.v6_0_connection_servers_status_descr
)
),
)
)
private val lastVersion = versionDescriptions.last().version
@@ -89,10 +89,7 @@ fun ConnectMobileLayout(
connectDesktop: () -> Unit,
deleteHost: (RemoteHostInfo) -> Unit,
) {
ColumnWithScrollBar(
Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles))
SectionView(generalGetString(MR.strings.this_device_name).uppercase()) {
DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) }
@@ -100,7 +97,7 @@ fun ConnectMobileLayout(
PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), checked = remember { controller.appPrefs.offerRemoteMulticast.state }.value) {
controller.appPrefs.offerRemoteMulticast.set(it)
}
SectionDividerSpaced(maxBottomPadding = false)
SectionDividerSpaced()
}
SectionView(stringResource(MR.strings.devices).uppercase()) {
if (chatModel.localUserCreated.value == true) {
@@ -179,10 +176,7 @@ private fun ConnectMobileViewLayout(
refreshQrCode: () -> Unit = {},
UnderQrLayout: @Composable () -> Unit = {},
) {
ColumnWithScrollBar(
Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
if (title != null) {
AppBarTitle(title)
}
@@ -2,39 +2,54 @@ package chat.simplex.common.views.usersettings
import SectionBottomSpacer
import SectionCustomFooter
import SectionDividerSpaced
import SectionItemView
import SectionItemWithValue
import SectionView
import SectionViewSelectableCards
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
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.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalDensity
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.common.platform.chatModel
import chat.simplex.res.MR
import java.text.DecimalFormat
@Composable
fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
val currentCfg = remember { mutableStateOf(chatModel.controller.getNetCfg()) }
fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> Unit, close: () -> Unit) {
val currentRemoteHost by remember { chatModel.currentRemoteHost }
val developerTools = remember { appPrefs.developerTools.get() }
// Will be actual once the screen is re-opened
val savedCfg = remember { mutableStateOf(controller.getNetCfg()) }
// Will have an edited state when the screen is re-opened
val currentCfg = remember { stateGetOrPut("currentCfg") { controller.getNetCfg() } }
val currentCfgVal = currentCfg.value // used only on initialization
val onionHosts = remember { mutableStateOf(currentCfgVal.onionHosts) }
val sessionMode = remember { mutableStateOf(currentCfgVal.sessionMode) }
val smpProxyMode = remember { mutableStateOf(currentCfgVal.smpProxyMode) }
val smpProxyFallback = remember { mutableStateOf(currentCfgVal.smpProxyFallback) }
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(currentCfgVal.useSocksProxy) }
val networkTCPConnectTimeout = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout) }
val networkTCPTimeout = remember { mutableStateOf(currentCfgVal.tcpTimeout) }
val networkTCPTimeoutPerKb = remember { mutableStateOf(currentCfgVal.tcpTimeoutPerKb) }
var networkRcvConcurrency = remember { mutableStateOf(currentCfgVal.rcvConcurrency) }
val networkRcvConcurrency = remember { mutableStateOf(currentCfgVal.rcvConcurrency) }
val networkSMPPingInterval = remember { mutableStateOf(currentCfgVal.smpPingInterval) }
val networkSMPPingCount = remember { mutableStateOf(currentCfgVal.smpPingCount) }
val networkEnableKeepAlive = remember { mutableStateOf(currentCfgVal.enableKeepAlive) }
@@ -63,11 +78,11 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
}
return NetCfg(
socksProxy = currentCfg.value.socksProxy,
hostMode = currentCfg.value.hostMode,
requiredHostMode = currentCfg.value.requiredHostMode,
sessionMode = currentCfg.value.sessionMode,
smpProxyMode = currentCfg.value.smpProxyMode,
smpProxyFallback = currentCfg.value.smpProxyFallback,
// hostMode = currentCfg.value.hostMode,
// requiredHostMode = currentCfg.value.requiredHostMode,
sessionMode = sessionMode.value,
smpProxyMode = smpProxyMode.value,
smpProxyFallback = smpProxyFallback.value,
tcpConnectTimeout = networkTCPConnectTimeout.value,
tcpTimeout = networkTCPTimeout.value,
tcpTimeoutPerKb = networkTCPTimeoutPerKb.value,
@@ -75,10 +90,14 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
tcpKeepAlive = tcpKeepAlive,
smpPingInterval = networkSMPPingInterval.value,
smpPingCount = networkSMPPingCount.value
)
).withOnionHosts(onionHosts.value)
}
fun updateView(cfg: NetCfg) {
onionHosts.value = cfg.onionHosts
sessionMode.value = cfg.sessionMode
smpProxyMode.value = cfg.smpProxyMode
smpProxyFallback.value = cfg.smpProxyFallback
networkTCPConnectTimeout.value = cfg.tcpConnectTimeout
networkTCPTimeout.value = cfg.tcpTimeout
networkTCPTimeoutPerKb.value = cfg.tcpTimeoutPerKb
@@ -97,40 +116,80 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
}
}
fun saveCfg(cfg: NetCfg) {
fun saveCfg(cfg: NetCfg, close: (() -> Unit)? = null) {
withBGApi {
chatModel.controller.apiSetNetworkConfig(cfg)
currentCfg.value = cfg
chatModel.controller.setNetCfg(cfg)
if (chatModel.controller.apiSetNetworkConfig(cfg)) {
currentCfg.value = cfg
savedCfg.value = cfg
chatModel.controller.setNetCfg(cfg)
close?.invoke()
}
}
}
fun reset() {
val newCfg = if (currentCfg.value.useSocksProxy) NetCfg.proxyDefaults else NetCfg.defaults
updateView(newCfg)
saveCfg(newCfg)
currentCfg.value = newCfg
}
AdvancedNetworkSettingsLayout(
networkTCPConnectTimeout,
networkTCPTimeout,
networkTCPTimeoutPerKb,
networkRcvConcurrency,
networkSMPPingInterval,
networkSMPPingCount,
networkEnableKeepAlive,
networkTCPKeepIdle,
networkTCPKeepIntvl,
networkTCPKeepCnt,
resetDisabled = if (currentCfg.value.useSocksProxy) currentCfg.value == NetCfg.proxyDefaults else currentCfg.value == NetCfg.defaults,
reset = { showUpdateNetworkSettingsDialog(::reset) },
footerDisabled = buildCfg() == currentCfg.value,
revert = { updateView(currentCfg.value) },
save = { showUpdateNetworkSettingsDialog { saveCfg(buildCfg()) } }
)
val saveDisabled = buildCfg() == savedCfg.value
ModalView(
close = {
if (saveDisabled) {
close()
} else {
showUnsavedChangesAlert({
saveCfg(buildCfg(), close)
}, close)
}
},
) {
AdvancedNetworkSettingsLayout(
currentRemoteHost = currentRemoteHost,
networkUseSocksProxy = networkUseSocksProxy,
developerTools = developerTools,
onionHosts = onionHosts,
useOnion = { onionHosts.value = it; currentCfg.value = currentCfg.value.withOnionHosts(it) },
sessionMode = sessionMode,
smpProxyMode = smpProxyMode,
smpProxyFallback = smpProxyFallback,
networkTCPConnectTimeout,
networkTCPTimeout,
networkTCPTimeoutPerKb,
networkRcvConcurrency,
networkSMPPingInterval,
networkSMPPingCount,
networkEnableKeepAlive,
networkTCPKeepIdle,
networkTCPKeepIntvl,
networkTCPKeepCnt,
updateSessionMode = { sessionMode.value = it; currentCfg.value = currentCfg.value.copy(sessionMode = it) },
updateSMPProxyMode = { smpProxyMode.value = it; currentCfg.value = currentCfg.value.copy(smpProxyMode = it) },
updateSMPProxyFallback = { smpProxyFallback.value = it; currentCfg.value = currentCfg.value.copy(smpProxyFallback = it) },
showModal = showModal,
resetDisabled = if (currentCfg.value.useSocksProxy) buildCfg() == NetCfg.proxyDefaults else buildCfg() == NetCfg.defaults,
reset = ::reset,
saveDisabled = saveDisabled,
save = {
showUpdateNetworkSettingsDialog {
saveCfg(buildCfg())
}
}
)
}
}
@Composable fun AdvancedNetworkSettingsLayout(
currentRemoteHost: RemoteHostInfo?,
networkUseSocksProxy: State<Boolean>,
developerTools: Boolean,
onionHosts: MutableState<OnionHosts>,
useOnion: (OnionHosts) -> Unit,
sessionMode: MutableState<TransportSessionMode>,
smpProxyMode: MutableState<SMPProxyMode>,
smpProxyFallback: MutableState<SMPProxyFallback>,
networkTCPConnectTimeout: MutableState<Long>,
networkTCPTimeout: MutableState<Long>,
networkTCPTimeoutPerKb: MutableState<Long>,
@@ -141,10 +200,13 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
networkTCPKeepIdle: MutableState<Int>,
networkTCPKeepIntvl: MutableState<Int>,
networkTCPKeepCnt: MutableState<Int>,
updateSessionMode: (TransportSessionMode) -> Unit,
updateSMPProxyMode: (SMPProxyMode) -> Unit,
updateSMPProxyFallback: (SMPProxyFallback) -> Unit,
showModal: (ModalData.() -> Unit) -> Unit,
resetDisabled: Boolean,
reset: () -> Unit,
footerDisabled: Boolean,
revert: () -> Unit,
saveDisabled: Boolean,
save: () -> Unit
) {
val secondsLabel = stringResource(MR.strings.network_option_seconds_label)
@@ -154,10 +216,39 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.network_settings_title))
SectionView {
SectionItemView {
ResetToDefaultsButton(reset, disabled = resetDisabled)
if (currentRemoteHost == null) {
SectionView(generalGetString(MR.strings.settings_section_title_private_message_routing)) {
SMPProxyModePicker(smpProxyMode, showModal, updateSMPProxyMode)
SMPProxyFallbackPicker(smpProxyFallback, showModal, updateSMPProxyFallback, enabled = remember { derivedStateOf { smpProxyMode.value != SMPProxyMode.Never } })
SettingsPreferenceItem(painterResource(MR.images.ic_arrow_forward), stringResource(MR.strings.private_routing_show_message_status), chatModel.controller.appPrefs.showSentViaProxy)
}
SectionCustomFooter {
Text(stringResource(MR.strings.private_routing_explanation))
}
SectionDividerSpaced(maxTopPadding = true)
}
if (currentRemoteHost == null && networkUseSocksProxy.value) {
SectionView(stringResource(MR.strings.network_socks_proxy).uppercase()) {
UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion)
SectionCustomFooter {
Column {
Text(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported))
}
}
}
SectionDividerSpaced(maxTopPadding = true)
}
if (currentRemoteHost == null && developerTools) {
SectionView(stringResource(MR.strings.network_session_mode_transport_isolation).uppercase()) {
SessionModePicker(sessionMode, showModal, updateSessionMode)
}
SectionDividerSpaced()
}
SectionView(stringResource(MR.strings.network_option_tcp_connection).uppercase()) {
SectionItemView {
TimeoutSettingRow(
stringResource(MR.strings.network_option_tcp_connection_timeout), networkTCPConnectTimeout,
@@ -220,23 +311,92 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
}
}
}
SectionCustomFooter {
SettingsSectionFooter(revert, save, footerDisabled)
SectionDividerSpaced(maxBottomPadding = false)
SectionView {
SectionItemView(reset, disabled = resetDisabled) {
Text(stringResource(MR.strings.network_options_reset_to_defaults), color = if (resetDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
}
SectionItemView(save, disabled = saveDisabled) {
Text(stringResource(MR.strings.network_options_save_and_reconnect), color = if (saveDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
}
}
SectionBottomSpacer()
}
}
@Composable
fun ResetToDefaultsButton(reset: () -> Unit, disabled: Boolean) {
val modifier = if (disabled) Modifier else Modifier.clickable { reset() }
Row(
modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically
) {
val color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
Text(stringResource(MR.strings.network_options_reset_to_defaults), color = color)
private fun SMPProxyModePicker(
smpProxyMode: MutableState<SMPProxyMode>,
showModal: (@Composable ModalData.() -> Unit) -> Unit,
updateSMPProxyMode: (SMPProxyMode) -> Unit,
) {
val density = LocalDensity.current
val values = remember {
SMPProxyMode.values().map {
when (it) {
SMPProxyMode.Always -> ValueTitleDesc(SMPProxyMode.Always, generalGetString(MR.strings.network_smp_proxy_mode_always), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_always_description), density))
SMPProxyMode.Unknown -> ValueTitleDesc(SMPProxyMode.Unknown, generalGetString(MR.strings.network_smp_proxy_mode_unknown), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_unknown_description), density))
SMPProxyMode.Unprotected -> ValueTitleDesc(SMPProxyMode.Unprotected, generalGetString(MR.strings.network_smp_proxy_mode_unprotected), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_unprotected_description), density))
SMPProxyMode.Never -> ValueTitleDesc(SMPProxyMode.Never, generalGetString(MR.strings.network_smp_proxy_mode_never), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_never_description), density))
}
}
}
SectionItemWithValue(
generalGetString(MR.strings.network_smp_proxy_mode_private_routing),
smpProxyMode,
values,
icon = painterResource(MR.images.ic_settings_ethernet),
onSelected = {
showModal {
ColumnWithScrollBar(
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.network_smp_proxy_mode_private_routing))
SectionViewSelectableCards(null, smpProxyMode, values, updateSMPProxyMode)
}
}
}
)
}
@Composable
private fun SMPProxyFallbackPicker(
smpProxyFallback: MutableState<SMPProxyFallback>,
showModal: (@Composable ModalData.() -> Unit) -> Unit,
updateSMPProxyFallback: (SMPProxyFallback) -> Unit,
enabled: State<Boolean>,
) {
val density = LocalDensity.current
val values = remember {
SMPProxyFallback.values().map {
when (it) {
SMPProxyFallback.Allow -> ValueTitleDesc(SMPProxyFallback.Allow, generalGetString(MR.strings.network_smp_proxy_fallback_allow), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_allow_description), density))
SMPProxyFallback.AllowProtected -> ValueTitleDesc(SMPProxyFallback.AllowProtected, generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected_description), density))
SMPProxyFallback.Prohibit -> ValueTitleDesc(SMPProxyFallback.Prohibit, generalGetString(MR.strings.network_smp_proxy_fallback_prohibit), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_prohibit_description), density))
}
}
}
SectionItemWithValue(
generalGetString(MR.strings.network_smp_proxy_fallback_allow_downgrade),
smpProxyFallback,
values,
icon = painterResource(MR.images.ic_arrows_left_right),
enabled = enabled,
onSelected = {
showModal {
ColumnWithScrollBar(
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.network_smp_proxy_fallback_allow_downgrade))
SectionViewSelectableCards(null, smpProxyFallback, values, updateSMPProxyFallback)
}
}
}
)
}
@Composable
@@ -377,43 +537,6 @@ fun TimeoutSettingRow(title: String, selection: MutableState<Long>, values: List
}
}
@Composable
fun SettingsSectionFooter(revert: () -> Unit, save: () -> Unit, disabled: Boolean) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
FooterButton(painterResource(MR.images.ic_replay), stringResource(MR.strings.network_options_revert), revert, disabled)
FooterButton(painterResource(MR.images.ic_check), stringResource(MR.strings.network_options_save), save, disabled)
}
}
@Composable
fun FooterButton(icon: Painter, title: String, action: () -> Unit, disabled: Boolean) {
Surface(
shape = RoundedCornerShape(20.dp),
color = Color.Black.copy(alpha = 0f),
contentColor = LocalContentColor.current
) {
val modifier = if (disabled) Modifier else Modifier.clickable { action() }
Row(
modifier.padding(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
icon,
title,
tint = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
)
Text(
title,
color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
)
}
}
}
fun showUpdateNetworkSettingsDialog(action: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.update_network_settings_question),
@@ -423,11 +546,27 @@ fun showUpdateNetworkSettingsDialog(action: () -> Unit) {
)
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.update_network_settings_question),
confirmText = generalGetString(MR.strings.network_options_save_and_reconnect),
dismissText = generalGetString(MR.strings.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)
}
@Preview
@Composable
fun PreviewAdvancedNetworkSettingsLayout() {
SimpleXTheme {
AdvancedNetworkSettingsLayout(
currentRemoteHost = null,
networkUseSocksProxy = remember { mutableStateOf(false) },
developerTools = false,
sessionMode = remember { mutableStateOf(TransportSessionMode.User) },
smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) },
smpProxyFallback = remember { mutableStateOf(SMPProxyFallback.Allow) },
networkTCPConnectTimeout = remember { mutableStateOf(10_000000) },
networkTCPTimeout = remember { mutableStateOf(10_000000) },
networkTCPTimeoutPerKb = remember { mutableStateOf(10_000) },
@@ -438,10 +577,15 @@ fun PreviewAdvancedNetworkSettingsLayout() {
networkTCPKeepIdle = remember { mutableStateOf(10) },
networkTCPKeepIntvl = remember { mutableStateOf(10) },
networkTCPKeepCnt = remember { mutableStateOf(10) },
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
useOnion = {},
updateSessionMode = {},
updateSMPProxyMode = {},
updateSMPProxyFallback = {},
showModal = {},
resetDisabled = false,
reset = {},
footerDisabled = false,
revert = {},
saveDisabled = false,
save = {}
)
}
@@ -36,10 +36,7 @@ fun CallSettingsLayout(
callOnLockScreen: SharedPreference<CallOnLockScreen>,
editIceServers: () -> Unit,
) {
ColumnWithScrollBar(
Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
AppBarTitle(stringResource(MR.strings.your_calls))
val lockCallState = remember { mutableStateOf(callOnLockScreen.get()) }
SectionView(stringResource(MR.strings.settings_section_title_settings)) {
@@ -7,9 +7,7 @@ import SectionItemView
import SectionItemWithValue
import SectionView
import SectionViewSelectable
import SectionViewSelectableCards
import TextIconSpaced
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.*
@@ -20,20 +18,17 @@ import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.input.*
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
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.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.ClickableText
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.helpers.annotatedStringResource
import chat.simplex.res.MR
@Composable
@@ -42,22 +37,11 @@ fun NetworkAndServersView() {
// It's not a state, just a one-time value. Shouldn't be used in any state-related situations
val netCfg = remember { chatModel.controller.getNetCfg() }
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(netCfg.useSocksProxy) }
val developerTools = chatModel.controller.appPrefs.developerTools.get()
val onionHosts = remember { mutableStateOf(netCfg.onionHosts) }
val sessionMode = remember { mutableStateOf(netCfg.sessionMode) }
val smpProxyMode = remember { mutableStateOf(netCfg.smpProxyMode) }
val smpProxyFallback = remember { mutableStateOf(netCfg.smpProxyFallback) }
val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } }
NetworkAndServersLayout(
currentRemoteHost = currentRemoteHost,
developerTools = developerTools,
networkUseSocksProxy = networkUseSocksProxy,
onionHosts = onionHosts,
sessionMode = sessionMode,
smpProxyMode = smpProxyMode,
smpProxyFallback = smpProxyFallback,
proxyPort = proxyPort,
toggleSocksProxy = { enable ->
val def = NetCfg.defaults
val proxyDef = NetCfg.proxyDefaults
@@ -84,7 +68,6 @@ fun NetworkAndServersView() {
chatModel.controller.apiSetNetworkConfig(conf)
chatModel.controller.setNetCfg(conf)
networkUseSocksProxy.value = true
onionHosts.value = conf.onionHosts
}
}
)
@@ -111,184 +94,48 @@ fun NetworkAndServersView() {
chatModel.controller.apiSetNetworkConfig(conf)
chatModel.controller.setNetCfg(conf)
networkUseSocksProxy.value = false
onionHosts.value = conf.onionHosts
}
}
)
}
},
useOnion = {
if (onionHosts.value == it) return@NetworkAndServersLayout
val prevValue = onionHosts.value
onionHosts.value = it
val startsWith = when (it) {
OnionHosts.NEVER -> generalGetString(MR.strings.network_use_onion_hosts_no_desc_in_alert)
OnionHosts.PREFER -> generalGetString(MR.strings.network_use_onion_hosts_prefer_desc_in_alert)
OnionHosts.REQUIRED -> generalGetString(MR.strings.network_use_onion_hosts_required_desc_in_alert)
}
showUpdateNetworkSettingsDialog(
title = generalGetString(MR.strings.update_onion_hosts_settings_question),
startsWith,
onDismiss = {
onionHosts.value = prevValue
}
) {
withBGApi {
val newCfg = chatModel.controller.getNetCfg().withOnionHosts(it)
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
if (res) {
chatModel.controller.setNetCfg(newCfg)
onionHosts.value = it
} else {
onionHosts.value = prevValue
}
}
}
},
updateSessionMode = {
if (sessionMode.value == it) return@NetworkAndServersLayout
val prevValue = sessionMode.value
sessionMode.value = it
val startsWith = when (it) {
TransportSessionMode.User -> generalGetString(MR.strings.network_session_mode_user_description)
TransportSessionMode.Entity -> generalGetString(MR.strings.network_session_mode_entity_description)
}
showUpdateNetworkSettingsDialog(
title = generalGetString(MR.strings.update_network_session_mode_question),
startsWith,
onDismiss = { sessionMode.value = prevValue }
) {
withBGApi {
val newCfg = chatModel.controller.getNetCfg().copy(sessionMode = it)
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
if (res) {
chatModel.controller.setNetCfg(newCfg)
sessionMode.value = it
} else {
sessionMode.value = prevValue
}
}
}
},
updateSMPProxyMode = {
if (smpProxyMode.value == it) return@NetworkAndServersLayout
val prevValue = smpProxyMode.value
smpProxyMode.value = it
val startsWith = when (it) {
SMPProxyMode.Always -> generalGetString(MR.strings.network_smp_proxy_mode_always_description)
SMPProxyMode.Unknown -> generalGetString(MR.strings.network_smp_proxy_mode_unknown_description)
SMPProxyMode.Unprotected -> generalGetString(MR.strings.network_smp_proxy_mode_unprotected_description)
SMPProxyMode.Never -> generalGetString(MR.strings.network_smp_proxy_mode_never_description)
}
showUpdateNetworkSettingsDialog(
title = generalGetString(MR.strings.update_network_smp_proxy_mode_question),
startsWith,
onDismiss = { smpProxyMode.value = prevValue }
) {
withBGApi {
val newCfg = chatModel.controller.getNetCfg().copy(smpProxyMode = it)
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
if (res) {
chatModel.controller.setNetCfg(newCfg)
smpProxyMode.value = it
} else {
smpProxyMode.value = prevValue
}
}
}
},
updateSMPProxyFallback = {
if (smpProxyFallback.value == it) return@NetworkAndServersLayout
val prevValue = smpProxyFallback.value
smpProxyFallback.value = it
val startsWith = when (it) {
SMPProxyFallback.Allow -> generalGetString(MR.strings.network_smp_proxy_fallback_allow_description)
SMPProxyFallback.AllowProtected -> generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected_description)
SMPProxyFallback.Prohibit -> generalGetString(MR.strings.network_smp_proxy_fallback_prohibit_description)
}
showUpdateNetworkSettingsDialog(
title = generalGetString(MR.strings.update_network_smp_proxy_fallback_question),
startsWith,
onDismiss = { smpProxyFallback.value = prevValue }
) {
withBGApi {
val newCfg = chatModel.controller.getNetCfg().copy(smpProxyFallback = it)
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
if (res) {
chatModel.controller.setNetCfg(newCfg)
smpProxyFallback.value = it
} else {
smpProxyFallback.value = prevValue
}
}
}
}
)
}
@Composable fun NetworkAndServersLayout(
currentRemoteHost: RemoteHostInfo?,
developerTools: Boolean,
networkUseSocksProxy: MutableState<Boolean>,
onionHosts: MutableState<OnionHosts>,
sessionMode: MutableState<TransportSessionMode>,
smpProxyMode: MutableState<SMPProxyMode>,
smpProxyFallback: MutableState<SMPProxyFallback>,
proxyPort: State<Int>,
toggleSocksProxy: (Boolean) -> Unit,
useOnion: (OnionHosts) -> Unit,
updateSessionMode: (TransportSessionMode) -> Unit,
updateSMPProxyMode: (SMPProxyMode) -> Unit,
updateSMPProxyFallback: (SMPProxyFallback) -> Unit,
) {
val m = chatModel
ColumnWithScrollBar(
Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) }
val showCustomModal = { it: @Composable (close: () -> Unit) -> Unit -> ModalManager.start.showCustomModal { close -> it(close) }}
AppBarTitle(stringResource(MR.strings.network_and_servers))
if (!chatModel.desktopNoUserNoRemote) {
SectionView(generalGetString(MR.strings.settings_section_title_messages)) {
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.smp_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) } })
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.message_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) } })
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) } })
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.media_and_file_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) } })
if (currentRemoteHost == null) {
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showModal, chatModel.controller.appPrefs.networkProxyHostPort, false)
UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion)
if (developerTools) {
SessionModePicker(sessionMode, showModal, updateSessionMode)
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, appPrefs.networkProxyHostPort, false, it) }})
SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), { ModalManager.start.showCustomModal { AdvancedNetworkSettingsView(showModal, it) } })
if (networkUseSocksProxy.value) {
SectionCustomFooter {
Column {
Text(annotatedStringResource(MR.strings.socks_proxy_setting_limitations))
}
}
SectionDividerSpaced(maxTopPadding = true)
} else {
SectionDividerSpaced()
}
SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), { ModalManager.start.showModal { AdvancedNetworkSettingsView(m) } })
}
}
}
if (currentRemoteHost == null && networkUseSocksProxy.value) {
SectionCustomFooter {
Column {
Text(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported))
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
Text(annotatedStringResource(MR.strings.socks_proxy_setting_limitations))
}
}
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
} else if (!chatModel.desktopNoUserNoRemote) {
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
}
if (currentRemoteHost == null) {
SectionView(generalGetString(MR.strings.settings_section_title_private_message_routing)) {
SMPProxyModePicker(smpProxyMode, showModal, updateSMPProxyMode)
SMPProxyFallbackPicker(smpProxyFallback, showModal, updateSMPProxyFallback, enabled = remember { mutableStateOf(smpProxyMode.value != SMPProxyMode.Never) })
SettingsPreferenceItem(painterResource(MR.images.ic_arrow_forward), stringResource(MR.strings.private_routing_show_message_status), chatModel.controller.appPrefs.showSentViaProxy)
}
SectionCustomFooter {
Text(stringResource(MR.strings.private_routing_explanation))
}
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
}
SectionView(generalGetString(MR.strings.settings_section_title_calls)) {
SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), { ModalManager.start.showModal { RTCServersView(m) } })
@@ -313,13 +160,14 @@ fun NetworkAndServersView() {
onionHosts: MutableState<OnionHosts>,
sessionMode: MutableState<TransportSessionMode>,
networkProxyHostPort: SharedPreference<String?>,
proxyPort: State<Int>,
toggleSocksProxy: (Boolean) -> Unit,
useOnion: (OnionHosts) -> Unit,
updateSessionMode: (TransportSessionMode) -> Unit,
) {
val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.fullscreen.showModal(content = it) }
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showModal, networkProxyHostPort, true)
val showCustomModal = { it: @Composable (close: () -> Unit) -> Unit -> ModalManager.fullscreen.showCustomModal { close -> it(close) }}
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, networkProxyHostPort, true, it) } })
UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion)
if (developerTools) {
SessionModePicker(sessionMode, showModal, updateSessionMode)
@@ -329,11 +177,7 @@ fun NetworkAndServersView() {
@Composable
fun UseSocksProxySwitch(
networkUseSocksProxy: MutableState<Boolean>,
proxyPort: State<Int>,
toggleSocksProxy: (Boolean) -> Unit,
showModal: (@Composable ModalData.() -> Unit) -> Unit,
networkProxyHostPort: SharedPreference<String?> = chatModel.controller.appPrefs.networkProxyHostPort,
migration: Boolean = false,
) {
Row(
Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING),
@@ -350,32 +194,7 @@ fun UseSocksProxySwitch(
tint = MaterialTheme.colors.secondary
)
TextIconSpaced(false)
val text = buildAnnotatedString {
append(generalGetString(MR.strings.network_socks_toggle_use_socks_proxy) + " (")
val style = SpanStyle(color = MaterialTheme.colors.primary)
val disabledStyle = SpanStyle(color = MaterialTheme.colors.onBackground)
withAnnotation(tag = "PORT", annotation = generalGetString(MR.strings.network_proxy_port).format(proxyPort.value)) {
withStyle(if (networkUseSocksProxy.value || !migration) style else disabledStyle) {
append(generalGetString(MR.strings.network_proxy_port).format(proxyPort.value))
}
}
append(")")
}
ClickableText(
text,
style = TextStyle(color = MaterialTheme.colors.onBackground, fontSize = 16.sp, fontFamily = Inter, fontWeight = FontWeight.Normal),
onClick = { offset ->
text.getStringAnnotations(tag = "PORT", start = offset, end = offset)
.firstOrNull()?.let { _ ->
if (networkUseSocksProxy.value || !migration) {
showModal { SockProxySettings(chatModel, networkProxyHostPort, migration) }
}
}
},
shouldConsumeEvent = { offset ->
text.getStringAnnotations(tag = "PORT", start = offset, end = offset).any()
}
)
Text(generalGetString(MR.strings.network_socks_toggle_use_socks_proxy))
}
DefaultSwitch(
checked = networkUseSocksProxy.value,
@@ -385,94 +204,124 @@ fun UseSocksProxySwitch(
}
@Composable
fun SockProxySettings(
m: ChatModel,
networkProxyHostPort: SharedPreference<String?> = m.controller.appPrefs.networkProxyHostPort,
fun SocksProxySettings(
networkUseSocksProxy: Boolean,
networkProxyHostPort: SharedPreference<String?> = appPrefs.networkProxyHostPort,
migration: Boolean,
close: () -> Unit
) {
ColumnWithScrollBar(
Modifier
.fillMaxWidth()
) {
val defaultHostPort = remember { "localhost:9050" }
AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings))
val hostPortSaved by remember { networkProxyHostPort.state }
val hostUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.firstOrNull() ?: "localhost"))
}
val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.lastOrNull() ?: "9050"))
}
val save = {
val defaultHostPort = remember { "localhost:9050" }
val hostPortSaved by remember { networkProxyHostPort.state }
val hostUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.firstOrNull() ?: "localhost"))
}
val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.lastOrNull() ?: "9050"))
}
val save = {
val oldValue = networkProxyHostPort.get()
networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
if (networkUseSocksProxy && !migration) {
withBGApi {
networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
if (m.controller.appPrefs.networkUseSocksProxy.get() && !migration) {
m.controller.apiSetNetworkConfig(m.controller.getNetCfg())
if (!controller.apiSetNetworkConfig(controller.getNetCfg())) {
networkProxyHostPort.set(oldValue)
}
}
}
SectionView {
SectionItemView {
ResetToDefaultsButton({
val reset = {
networkProxyHostPort.set(defaultHostPort)
val newHost = defaultHostPort.split(":").first()
val newPort = defaultHostPort.split(":").last()
hostUnsaved.value = hostUnsaved.value.copy(newHost, TextRange(newHost.length))
portUnsaved.value = portUnsaved.value.copy(newPort, TextRange(newPort.length))
save()
}
if (m.controller.appPrefs.networkUseSocksProxy.get() && !migration) {
showUpdateNetworkSettingsDialog {
reset()
}
} else {
reset()
}
}, disabled = hostPortSaved == defaultHostPort)
}
SectionItemView {
DefaultConfigurableTextField(
hostUnsaved,
stringResource(MR.strings.host_verb),
modifier = Modifier.fillMaxWidth(),
isValid = ::validHost,
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
keyboardType = KeyboardType.Text,
)
}
SectionItemView {
DefaultConfigurableTextField(
portUnsaved,
stringResource(MR.strings.port_verb),
modifier = Modifier.fillMaxWidth(),
isValid = ::validPort,
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save() }),
keyboardType = KeyboardType.Number,
)
}
val saveAndClose = {
val oldValue = networkProxyHostPort.get()
networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
if (networkUseSocksProxy && !migration) {
withBGApi {
if (controller.apiSetNetworkConfig(controller.getNetCfg())) {
close()
} else {
networkProxyHostPort.set(oldValue)
}
}
}
SectionCustomFooter {
NetworkSectionFooter(
revert = {
val prevHost = hostPortSaved?.split(":")?.firstOrNull() ?: "localhost"
val prevPort = hostPortSaved?.split(":")?.lastOrNull() ?: "9050"
hostUnsaved.value = hostUnsaved.value.copy(prevHost, TextRange(prevHost.length))
portUnsaved.value = portUnsaved.value.copy(prevPort, TextRange(prevPort.length))
},
save = { if (m.controller.appPrefs.networkUseSocksProxy.get() && !migration) showUpdateNetworkSettingsDialog { save() } else save() },
revertDisabled = hostPortSaved == (hostUnsaved.value.text + ":" + portUnsaved.value.text),
saveDisabled = hostPortSaved == (hostUnsaved.value.text + ":" + portUnsaved.value.text) ||
remember { derivedStateOf { !validHost(hostUnsaved.value.text) } }.value ||
remember { derivedStateOf { !validPort(portUnsaved.value.text) } }.value
)
}
val saveDisabled = hostPortSaved == (hostUnsaved.value.text + ":" + portUnsaved.value.text) ||
remember { derivedStateOf { !validHost(hostUnsaved.value.text) } }.value ||
remember { derivedStateOf { !validPort(portUnsaved.value.text) } }.value
val resetDisabled = hostUnsaved.value.text + ":" + portUnsaved.value.text == defaultHostPort
ModalView(
close = {
if (saveDisabled) {
close()
} else {
showUnsavedSocksHostPortAlert(
confirmText = generalGetString(if (networkUseSocksProxy && !migration) MR.strings.network_options_save_and_reconnect else MR.strings.network_options_save),
save = saveAndClose,
close = close
)
}
},
) {
ColumnWithScrollBar(
Modifier
.fillMaxWidth()
) {
AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings))
SectionView {
SectionItemView {
DefaultConfigurableTextField(
hostUnsaved,
stringResource(MR.strings.host_verb),
modifier = Modifier.fillMaxWidth(),
isValid = ::validHost,
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
keyboardType = KeyboardType.Text,
)
}
SectionItemView {
DefaultConfigurableTextField(
portUnsaved,
stringResource(MR.strings.port_verb),
modifier = Modifier.fillMaxWidth(),
isValid = ::validPort,
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save() }),
keyboardType = KeyboardType.Number,
)
}
}
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 27.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
SectionView {
SectionItemView({
val newHost = defaultHostPort.split(":").first()
val newPort = defaultHostPort.split(":").last()
hostUnsaved.value = hostUnsaved.value.copy(newHost, TextRange(newHost.length))
portUnsaved.value = portUnsaved.value.copy(newPort, TextRange(newPort.length))
}, disabled = resetDisabled) {
Text(stringResource(MR.strings.network_options_reset_to_defaults), color = if (resetDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
}
SectionItemView(
click = { if (networkUseSocksProxy && !migration) showUpdateNetworkSettingsDialog { save() } else save() },
disabled = saveDisabled
) {
Text(stringResource(if (networkUseSocksProxy && !migration) MR.strings.network_options_save_and_reconnect else MR.strings.network_options_save), color = if (saveDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
}
}
SectionBottomSpacer()
}
SectionBottomSpacer()
}
}
private fun showUnsavedSocksHostPortAlert(confirmText: String, save: () -> Unit, close: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.update_network_settings_question),
confirmText = confirmText,
dismissText = generalGetString(MR.strings.exit_without_saving),
onConfirm = save,
onDismiss = close,
)
}
@Composable
private fun UseOnionHosts(
fun UseOnionHosts(
onionHosts: MutableState<OnionHosts>,
enabled: State<Boolean>,
showModal: (@Composable ModalData.() -> Unit) -> Unit,
@@ -521,7 +370,7 @@ private fun UseOnionHosts(
}
@Composable
private fun SessionModePicker(
fun SessionModePicker(
sessionMode: MutableState<TransportSessionMode>,
showModal: (@Composable ModalData.() -> Unit) -> Unit,
updateSessionMode: (TransportSessionMode) -> Unit,
@@ -554,91 +403,6 @@ private fun SessionModePicker(
)
}
@Composable
private fun SMPProxyModePicker(
smpProxyMode: MutableState<SMPProxyMode>,
showModal: (@Composable ModalData.() -> Unit) -> Unit,
updateSMPProxyMode: (SMPProxyMode) -> Unit,
) {
val density = LocalDensity.current
val values = remember {
SMPProxyMode.values().map {
when (it) {
SMPProxyMode.Always -> ValueTitleDesc(SMPProxyMode.Always, generalGetString(MR.strings.network_smp_proxy_mode_always), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_always_description), density))
SMPProxyMode.Unknown -> ValueTitleDesc(SMPProxyMode.Unknown, generalGetString(MR.strings.network_smp_proxy_mode_unknown), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_unknown_description), density))
SMPProxyMode.Unprotected -> ValueTitleDesc(SMPProxyMode.Unprotected, generalGetString(MR.strings.network_smp_proxy_mode_unprotected), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_unprotected_description), density))
SMPProxyMode.Never -> ValueTitleDesc(SMPProxyMode.Never, generalGetString(MR.strings.network_smp_proxy_mode_never), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_never_description), density))
}
}
}
SectionItemWithValue(
generalGetString(MR.strings.network_smp_proxy_mode_private_routing),
smpProxyMode,
values,
icon = painterResource(MR.images.ic_settings_ethernet),
onSelected = {
showModal {
ColumnWithScrollBar(
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.network_smp_proxy_mode_private_routing))
SectionViewSelectableCards(null, smpProxyMode, values, updateSMPProxyMode)
}
}
}
)
}
@Composable
private fun SMPProxyFallbackPicker(
smpProxyFallback: MutableState<SMPProxyFallback>,
showModal: (@Composable ModalData.() -> Unit) -> Unit,
updateSMPProxyFallback: (SMPProxyFallback) -> Unit,
enabled: State<Boolean>,
) {
val density = LocalDensity.current
val values = remember {
SMPProxyFallback.values().map {
when (it) {
SMPProxyFallback.Allow -> ValueTitleDesc(SMPProxyFallback.Allow, generalGetString(MR.strings.network_smp_proxy_fallback_allow), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_allow_description), density))
SMPProxyFallback.AllowProtected -> ValueTitleDesc(SMPProxyFallback.AllowProtected, generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected_description), density))
SMPProxyFallback.Prohibit -> ValueTitleDesc(SMPProxyFallback.Prohibit, generalGetString(MR.strings.network_smp_proxy_fallback_prohibit), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_prohibit_description), density))
}
}
}
SectionItemWithValue(
generalGetString(MR.strings.network_smp_proxy_fallback_allow_downgrade),
smpProxyFallback,
values,
icon = painterResource(MR.images.ic_arrows_left_right),
enabled = enabled,
onSelected = {
showModal {
ColumnWithScrollBar(
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.network_smp_proxy_fallback_allow_downgrade))
SectionViewSelectableCards(null, smpProxyFallback, values, updateSMPProxyFallback)
}
}
}
)
}
@Composable
private fun NetworkSectionFooter(revert: () -> Unit, save: () -> Unit, revertDisabled: Boolean, saveDisabled: Boolean) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
FooterButton(painterResource(MR.images.ic_replay), stringResource(MR.strings.network_options_revert), revert, revertDisabled)
FooterButton(painterResource(MR.images.ic_check), stringResource(MR.strings.network_options_save), save, saveDisabled)
}
}
// https://stackoverflow.com/a/106223
private fun validHost(s: String): Boolean {
val validIp = Regex("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])[.]){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$")
@@ -652,7 +416,7 @@ fun validPort(s: String): Boolean {
return s.isNotBlank() && s.matches(validPort)
}
private fun showUpdateNetworkSettingsDialog(
fun showUpdateNetworkSettingsDialog(
title: String,
startsWith: String = "",
message: String = generalGetString(MR.strings.updating_settings_will_reconnect_client_to_all_servers),
@@ -675,18 +439,8 @@ fun PreviewNetworkAndServersLayout() {
SimpleXTheme {
NetworkAndServersLayout(
currentRemoteHost = null,
developerTools = true,
networkUseSocksProxy = remember { mutableStateOf(true) },
proxyPort = remember { mutableStateOf(9050) },
toggleSocksProxy = {},
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
sessionMode = remember { mutableStateOf(TransportSessionMode.User) },
smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) },
smpProxyFallback = remember { mutableStateOf(SMPProxyFallback.Allow) },
useOnion = {},
updateSessionMode = {},
updateSMPProxyMode = {},
updateSMPProxyFallback = {},
)
}
}
@@ -112,7 +112,7 @@ fun SettingsLayout(
Modifier
.fillMaxSize()
.themedBackground(theme.value.base)
.padding(top = if (appPlatform.isAndroid) DEFAULT_PADDING else DEFAULT_PADDING * 3)
.padding(top = if (appPlatform.isAndroid) DEFAULT_PADDING else DEFAULT_PADDING * 2.8f)
) {
AppBarTitle(stringResource(MR.strings.your_settings))
@@ -334,7 +334,7 @@
<string name="error_changing_role">خطأ في تغيير الدور</string>
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d فشل فك تعمية الرسائل.</string>
<string name="dark_theme">سمة داكنة</string>
<string name="deleted_description">حُذِفت</string>
<string name="deleted_description">حُذفت</string>
<string name="database_passphrase_and_export">عبارة مرور قاعدة البيانات وتصديرها</string>
<string name="delete_files_and_media_all">حذف جميع الملفات</string>
<string name="delete_after">حذف بعد</string>
@@ -353,8 +353,8 @@
<string name="button_delete_group">حذف المجموعة</string>
<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="share_text_deleted_at">حُذفت في: %s</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>
@@ -398,7 +398,7 @@
<string name="current_passphrase">عبارة المرور الحالية…</string>
<string name="database_encryption_will_be_updated">سيتم تحديث عبارة مرور تعمية قاعدة البيانات وتخزينها في Keystore.</string>
<string name="info_row_database_id">معرّف قاعدة البيانات</string>
<string name="info_row_deleted_at">حُذِفت في</string>
<string name="info_row_deleted_at">حُذفت في</string>
<string name="ttl_d">%d يوم</string>
<string name="ttl_days">%d أيام</string>
<string name="custom_time_picker_custom">مخصص</string>
@@ -773,7 +773,6 @@
<string name="old_database_archive">أرشيف قاعدة البيانات القديمة</string>
<string name="feature_off">غير مفعّل</string>
<string name="one_time_link">رابط دعوة لمرة واحدة</string>
<string name="network_use_onion_hosts_required_desc_in_alert">سوف تكون مضيفات البصل مطلوبة للاتصال.</string>
<string name="group_member_role_observer">المراقب</string>
<string name="item_info_no_text">لا يوجد نص</string>
<string name="new_member_role">دور عضو جديد</string>
@@ -789,8 +788,6 @@
<string name="network_use_onion_hosts_no">لا</string>
<string name="network_use_onion_hosts_required_desc">سوف تكون مضيفات البصل مطلوبة للاتصال.
\nيُرجى ملاحظة: أنك لن تتمكن من الاتصال بالخوادم بدون عنوان onion.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">سيتم استخدام مضيفات البصل عند توفرها.</string>
<string name="network_use_onion_hosts_no_desc_in_alert">لن يتم استخدام مضيفات البصل.</string>
<string name="self_destruct_new_display_name">اسم عرض جديد:</string>
<string name="new_passphrase">عبارة مرور جديدة…</string>
<string name="icon_descr_server_status_pending">قيد الانتظار</string>
@@ -892,7 +889,6 @@
<string name="info_row_updated_at">حٌديثت السجل في</string>
<string name="share_text_updated_at">حٌديثت السجل في: %s</string>
<string name="restore_database_alert_confirm">استعادة</string>
<string name="network_options_revert">إرجاع</string>
<string name="v4_4_live_messages_desc">يرى المستلمون التحديثات أثناء كتابتها.</string>
<string name="feature_received_prohibited">استلمت، ممنوع</string>
<string name="save_servers_button">حفظ</string>
@@ -1165,7 +1161,6 @@
<string name="your_chat_profiles">ملفات تعريف الدردشة الخاصة بك</string>
<string name="your_simplex_contact_address">عنوان SimpleX الخاص بك</string>
<string name="your_SMP_servers">خوادم SMP الخاصة بك</string>
<string name="update_onion_hosts_settings_question">هل تريد تحديث إعداد مضيفي onion.؟</string>
<string name="onboarding_notifications_mode_off">عندما يكون التطبيق قيد التشغيل</string>
<string name="call_connection_via_relay">عبر المُرحل</string>
<string name="you_joined_this_group">لقد انضممت إلى هذه المجموعة</string>
@@ -1403,7 +1398,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>
@@ -1930,7 +1925,7 @@
<string name="connections">الاتصالات</string>
<string name="created">أُنشئت</string>
<string name="decryption_errors">أخطاء فك التعمية</string>
<string name="deleted">حُذِفت</string>
<string name="deleted">حُذفت</string>
<string name="downloaded_files">الملفات التي نُزّلت</string>
<string name="download_errors">أخطاء التنزيل</string>
<string name="expired_label">منتهية الصلاحيّة</string>
@@ -1998,4 +1993,36 @@
<string name="privacy_media_blur_radius">تمويه الوسائط</string>
<string name="privacy_media_blur_radius_medium">متوسط</string>
<string name="privacy_media_blur_radius_soft">ناعم</string>
<string name="info_view_call_button">المكالمة</string>
<string name="info_view_connect_button">اتصل</string>
<string name="info_view_message_button">الرسالة</string>
<string name="info_view_open_button">افتح</string>
<string name="info_view_search_button">بحث</string>
<string name="toolbar_settings">الإعدادات</string>
<string name="info_view_video_button">فيديو</string>
<string name="confirm_delete_contact_question">تأكيد حذف جهة الاتصال؟</string>
<string name="contact_deleted">حُذفت جهة الاتصال!</string>
<string name="delete_contact_cannot_undo_warning">سيتم حذف جهة الاتصال - لا يمكن التراجع عن هذا!</string>
<string name="conversation_deleted">حُذفت المحادثة!</string>
<string name="delete_without_notification">احذف دون إشعار</string>
<string name="keep_conversation">أبقِ المحادثة</string>
<string name="only_delete_conversation">احذف المحادثة فقط</string>
<string name="you_can_still_send_messages_to_contact">لا يزال بإمكانك إرسال الرسائل إلى %1$s من الدردشات المحذوفة.</string>
<string name="paste_link">ألصق الرابط</string>
<string name="contact_list_header_title">جهات اتصالك</string>
<string name="one_hand_ui">واجهة مستخدم بيد واحدة</string>
<string name="cant_call_contact_deleted_alert_text">حُذفت جهة الاتصال.</string>
<string name="allow_calls_question">السماح بالمكالمات؟</string>
<string name="cant_call_member_send_message_alert_text">أرسل رسالة لتفعيل المكالمات.</string>
<string name="calls_prohibited_alert_title">المكالمات ممنوعة!</string>
<string name="cant_call_member_alert_title">لا يمكن مكالمة أحد أعضاء المجموعة</string>
<string name="cant_send_message_to_member_alert_title">لا يمكن إرسال رسالة إلى عضو المجموعة</string>
<string name="cant_call_contact_connecting_wait_alert_text">جارِ الاتصال بجهة الاتصال، يُرجى الانتظار أو التحقق لاحقًا!</string>
<string name="deleted_chats">حُذفت الدردشات</string>
<string name="action_button_add_members">ادعُ</string>
<string name="no_filtered_contacts">لا توجد جهات اتصال مُصفاة</string>
<string name="cant_call_contact_alert_title">لا يمكن مكالمة جهة الاتصال</string>
<string name="you_can_still_view_conversation_with_contact">لا يزال بإمكانك عرض المحادثة مع %1$s في قائمة الدردشات.</string>
<string name="you_need_to_allow_calls">يجب عليك السماح لجهات اتصالك بالاتصال حتى تتمكن من الاتصال بها.</string>
<string name="calls_prohibited_ask_to_enable_calls_alert_text">يُرجى الطلب من جهة اتصالك تفعيل المكالمات.</string>
</resources>
@@ -311,14 +311,19 @@
<string name="hide_verb">Hide</string>
<string name="allow_verb">Allow</string>
<string name="moderate_verb">Moderate</string>
<string name="select_verb">Select</string>
<string name="expand_verb">Expand</string>
<string name="delete_message__question">Delete message?</string>
<string name="delete_messages__question">Delete %d messages?</string>
<string name="delete_message_cannot_be_undone_warning">Message will be deleted - this cannot be undone!</string>
<string name="delete_message_mark_deleted_warning">Message will be marked for deletion. The recipient(s) will be able to reveal this message.</string>
<string name="delete_messages_mark_deleted_warning">Messages will be marked for deletion. The recipient(s) will be able to reveal these messages.</string>
<string name="delete_member_message__question">Delete member message?</string>
<string name="delete_members_messages__question">Delete %d messages of members?</string>
<string name="moderate_message_will_be_deleted_warning">The message will be deleted for all members.</string>
<string name="moderate_messages_will_be_deleted_warning">The messages will be deleted for all members.</string>
<string name="moderate_message_will_be_marked_warning">The message will be marked as moderated for all members.</string>
<string name="moderate_messages_will_be_marked_warning">The messages will be marked as moderated for all members.</string>
<string name="for_me_only">Delete for me</string>
<string name="for_everybody">For everyone</string>
<string name="stop_file__action">Stop file</string>
@@ -368,6 +373,8 @@
<!-- ChatView.kt -->
<string name="no_selected_chat">No selected chat</string>
<string name="selected_chat_items_nothing_selected">Nothing selected</string>
<string name="selected_chat_items_selected_n">Selected %d</string>
<!-- ShareListView.kt -->
<string name="share_message">Share message…</string>
@@ -398,6 +405,7 @@
<string name="simplex_links_not_allowed">SimpleX links not allowed</string>
<string name="files_and_media_not_allowed">Files and media not allowed</string>
<string name="voice_messages_not_allowed">Voice messages not allowed</string>
<string name="compose_message_placeholder">Message</string>
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
<string name="image_descr">Image</string>
@@ -459,7 +467,7 @@
<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="you_can_still_send_messages_to_contact">You can send messages to %1$s from Archived contacts.</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>
@@ -668,7 +676,7 @@
<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="deleted_chats">Archived contacts</string>
<string name="no_filtered_contacts">No filtered contacts</string>
<string name="contact_list_header_title">Your contacts</string>
@@ -699,6 +707,7 @@
<string name="send_us_an_email">Send us email</string>
<string name="chat_lock">SimpleX Lock</string>
<string name="chat_console">Chat console</string>
<string name="message_servers">Message servers</string>
<string name="smp_servers">SMP servers</string>
<string name="smp_servers_configured">Configured SMP servers</string>
<string name="smp_servers_other">Other SMP servers</string>
@@ -723,6 +732,8 @@
<string name="smp_servers_delete_server">Delete server</string>
<string name="smp_servers_per_user">The servers for new connections of your current chat profile</string>
<string name="smp_save_servers_question">Save servers?</string>
<string name="update_network_settings_question">Update network settings?</string>
<string name="media_and_file_servers">Media &amp; file servers</string>
<string name="xftp_servers">XFTP servers</string>
<string name="xftp_servers_configured">Configured XFTP servers</string>
<string name="xftp_servers_other">Other XFTP servers</string>
@@ -746,7 +757,8 @@
<string name="save_servers_button">Save</string>
<string name="network_and_servers">Network &amp; servers</string>
<string name="network_settings">Advanced network settings</string>
<string name="network_settings_title">Network settings</string>
<string name="network_settings_title">Advanced settings</string>
<string name="network_socks_proxy">SOCKS proxy</string>
<string name="network_socks_proxy_settings">SOCKS proxy settings</string>
<string name="network_socks_toggle_use_socks_proxy">Use SOCKS proxy</string>
<string name="network_proxy_port">port %d</string>
@@ -756,7 +768,6 @@
<string name="network_enable_socks_info">Access the servers via SOCKS proxy on port %d? Proxy must be started before enabling this option.</string>
<string name="network_disable_socks">Use direct Internet connection?</string>
<string name="network_disable_socks_info">If you confirm, the messaging servers will be able to see your IP address, and your provider - which servers you are connecting to.</string>
<string name="update_onion_hosts_settings_question">Update .onion hosts setting?</string>
<string name="network_use_onion_hosts">Use .onion hosts</string>
<string name="network_use_onion_hosts_prefer">When available</string>
<string name="network_use_onion_hosts_no">No</string>
@@ -764,9 +775,6 @@
<string name="network_use_onion_hosts_prefer_desc">Onion hosts will be used when available.</string>
<string name="network_use_onion_hosts_no_desc">Onion hosts will not be used.</string>
<string name="network_use_onion_hosts_required_desc">Onion hosts will be required for connection.\nPlease note: you will not be able to connect to the servers without .onion address.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion hosts will be used when available.</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion hosts will not be used.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Onion hosts will be required for connection.</string>
<string name="network_session_mode_transport_isolation">Transport isolation</string>
<string name="network_session_mode_user">Chat profile</string>
<string name="network_session_mode_entity">Connection</string>
@@ -777,7 +785,7 @@
<string name="socks_proxy_setting_limitations"><![CDATA[<b>Please note</b>: message and file relays are connected via SOCKS proxy. Calls and sending link previews use direct connection.]]></string>
<string name="network_smp_proxy_mode_private_routing">Private routing</string>
<string name="network_smp_proxy_mode_always">Always</string>
<string name="network_smp_proxy_mode_unknown">Unknown relays</string>
<string name="network_smp_proxy_mode_unknown">Unknown servers</string>
<string name="network_smp_proxy_mode_unprotected">Unprotected</string>
<string name="network_smp_proxy_mode_never">Never</string>
<string name="network_smp_proxy_mode_always_description">Always use private routing.</string>
@@ -1188,7 +1196,7 @@
<string name="error_importing_database">Error importing chat database</string>
<string name="chat_database_imported">Chat database imported</string>
<string name="restart_the_app_to_use_imported_chat_database">Restart the app to use imported chat database.</string>
<string name="non_fatal_errors_occured_during_import">Some non-fatal errors occurred during import - you may see Chat console for more details.</string>
<string name="non_fatal_errors_occured_during_import">Some non-fatal errors occurred during import:</string>
<string name="delete_chat_profile_question">Delete chat profile?</string>
<string name="delete_chat_profile_action_cannot_be_undone_warning">This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</string>
<string name="chat_database_deleted">Chat database deleted</string>
@@ -1214,6 +1222,11 @@
<string name="enable_automatic_deletion_message">This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes.</string>
<string name="delete_messages">Delete messages</string>
<string name="error_changing_message_deletion">Error changing setting</string>
<string name="chat_database_exported_title">Chat database exported</string>
<string name="chat_database_exported_save">You may save the exported archive.</string>
<string name="chat_database_exported_migrate">You may migrate the exported database.</string>
<string name="chat_database_exported_not_all_files">Some file(s) were not exported</string>
<string name="chat_database_exported_continue">Continue</string>
<!-- DatabaseEncryptionView.kt -->
<string name="save_passphrase_in_keychain">Save passphrase in Keystore</string>
@@ -1280,7 +1293,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="one_hand_ui">Reachable chat toolbar</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>
@@ -1599,6 +1612,7 @@
<string name="error_saving_group_profile">Error saving group profile</string>
<!-- AdvancedNetworkSettings.kt -->
<string name="network_option_tcp_connection">TCP connection</string>
<string name="network_options_reset_to_defaults">Reset to defaults</string>
<string name="network_option_seconds_label">sec</string>
<string name="network_option_tcp_connection_timeout">TCP connection timeout</string>
@@ -1608,8 +1622,8 @@
<string name="network_option_ping_interval">PING interval</string>
<string name="network_option_ping_count">PING count</string>
<string name="network_option_enable_tcp_keep_alive">Enable TCP keep-alive</string>
<string name="network_options_revert">Revert</string>
<string name="network_options_save">Save</string>
<string name="network_options_save_and_reconnect">Save and reconnect</string>
<string name="update_network_settings_question">Update network settings?</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">Updating settings will re-connect the client to all servers.</string>
<string name="update_network_settings_confirmation">Update</string>
@@ -1960,6 +1974,17 @@
<string name="v5_8_message_delivery">Improved message delivery</string>
<string name="v5_8_message_delivery_descr">With reduced battery usage.</string>
<string name="v5_8_persian_ui">Persian UI</string>
<string name="v6_0_private_routing_descr">It protects your IP address and connections.</string>
<string name="v6_0_your_contacts">Your contacts your way</string>
<string name="v6_0_your_contacts_descr">- Search contacts when starting chat.\n- Archive contacts to chat later.</string>
<string name="v6_0_reachable_chat_toolbar">Reachable chat toolbar 👋</string>
<string name="v6_0_reachable_chat_toolbar_descr">Use the app with one hand.</string>
<string name="v6_0_connect_faster">Connect to your friends faster</string>
<string name="v6_0_connect_faster_descr">Even when they are offline.</string>
<string name="v6_0_delete_many_messages">Moderate like a pro ✋</string>
<string name="v6_0_delete_many_messages_descr">Delete up to 20 messages at once.</string>
<string name="v6_0_connection_servers_status">Control your network</string>
<string name="v6_0_connection_servers_status_descr">Connection and servers status.</string>
<!-- CustomTimePicker -->
<string name="custom_time_unit_seconds">seconds</string>
@@ -800,13 +800,11 @@
<string name="network_settings_title">Мрежови настройки</string>
<string name="port_verb">Порт</string>
<string name="network_proxy_port">порт %d</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Няма се използват Onion хостове.</string>
<string name="network_use_onion_hosts_required">Задължително</string>
<string name="network_use_onion_hosts_no">Не</string>
<string name="network_use_onion_hosts_required_desc">За свързване ще са необходими Onion хостове.
\nМоля, обърнете внимание: няма да можете да се свържете със сървърите без .onion адрес.</string>
<string name="network_use_onion_hosts_prefer_desc">Ще се използват Onion хостове, когато са налични.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Ще се използват Onion хостове, когато са налични.</string>
<string name="network_use_onion_hosts_no_desc">Няма се използват Onion хостове.</string>
<string name="email_invite_subject">Нека да поговорим в SimpleX Chat</string>
<string name="password_to_show">Парола за показване</string>
@@ -878,7 +876,6 @@
<string name="no_selected_chat">Няма избран чат</string>
<string name="notifications">Известия</string>
<string name="shutdown_alert_desc">Известията ще спрат да работят, докато не стартирате отново приложението</string>
<string name="network_use_onion_hosts_required_desc_in_alert">За свързване ще са необходими Onion хостове.</string>
<string name="images_limit_desc">Само 10 изображения могат да бъдат изпратени едновременно</string>
<string name="only_owners_can_enable_files_and_media">Само собствениците на групата могат да активират файлове и медията.</string>
<string name="only_group_owners_can_enable_voice">Само собствениците на групата могат да активират гласови съобщения.</string>
@@ -1147,7 +1144,6 @@
<string name="icon_descr_speaker_off">Високоговорителят е изключен</string>
<string name="stop_chat_to_enable_database_actions">Спрете чата, за да активирате действията с базата данни.</string>
<string name="role_in_group">Роля</string>
<string name="network_options_revert">Отмени промените</string>
<string name="network_options_save">Запази</string>
<string name="reset_color">Нулирай цветовете</string>
<string name="color_secondary">Вторичен</string>
@@ -1225,7 +1221,6 @@
<string name="your_SMP_servers">Вашите SMP сървъри</string>
<string name="network_socks_toggle_use_socks_proxy">Използвай SOCKS прокси</string>
<string name="network_enable_socks">Използвай SOCKS прокси\?</string>
<string name="update_onion_hosts_settings_question">Актуализиране на настройката за .onion хостове\?</string>
<string name="network_disable_socks">Използване на директна интернет връзка\?</string>
<string name="network_use_onion_hosts">Използвай .onion хостове</string>
<string name="network_use_onion_hosts_prefer">Когато са налични</string>
@@ -228,7 +228,6 @@
<string name="network_enable_socks">Použít proxy server SOCKS\?</string>
<string name="network_disable_socks">Použít přímé připojení k internetu\?</string>
<string name="network_use_onion_hosts_no">Ne</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion hostitelé nebudou použiti.</string>
<string name="network_session_mode_user">Chat profil</string>
<string name="network_session_mode_entity">Připojení</string>
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
@@ -357,7 +356,6 @@
<string name="no_contacts_selected">Nebyl vybrán žádný kontakt</string>
<string name="invite_prohibited_description">Snažíte se pozvat kontakt, se kterým jste sdíleli inkognito profil, do skupiny, ve které používáte svůj hlavní profil</string>
<string name="info_row_group">Skupina</string>
<string name="network_options_revert">Vrátit</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">Aktualizací nastavení se klient znovu připojí ke všem serverům.</string>
<string name="accept_feature_set_1_day">Nastavit 1 den</string>
<string name="connection_error_auth">Chyba spojení (AUTH)</string>
@@ -629,14 +627,11 @@
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Ujistěte se, že adresy serverů WebRTC ICE jsou ve správném formátu, oddělené na řádcích a nejsou duplicitní.</string>
<string name="save_servers_button">Uložit</string>
<string name="network_and_servers">Síť a servery</string>
<string name="update_onion_hosts_settings_question">Aktualizovat nastavení hostitelů .onion\?</string>
<string name="network_use_onion_hosts">Použít hostitele .onion</string>
<string name="network_use_onion_hosts_prefer">Když bude dostupný</string>
<string name="network_use_onion_hosts_required">Povinné</string>
<string name="network_use_onion_hosts_prefer_desc">Onion hostitelé budou použiti, pokud jsou k dispozici.</string>
<string name="network_use_onion_hosts_no_desc">Onion hostitelé nebudou použiti.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion hostitelé budou použiti, pokud jsou k dispozici.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Pro připojení budou vyžadováni Onion hostitelé.</string>
<string name="network_session_mode_transport_isolation">Izolace přenosu</string>
<string name="network_session_mode_user_description"><![CDATA[A separate TCP connection (and SOCKS credential) will be used <b>for each chat profile you have in the app</b>.]]></string>
<string name="network_session_mode_entity_description">Oddělit TCP připojení (a SOCKS pověření) bude použito <b>pro všechny kontakty a členy skupin</b>.
@@ -396,7 +396,6 @@
<string name="network_enable_socks_info">Zugriff auf die Server über SOCKS-Proxy auf Port %d? Der Proxy muss gestartet werden, bevor diese Option aktiviert wird.</string>
<string name="network_disable_socks">Direkte Internetverbindung verwenden?</string>
<string name="network_disable_socks_info">Wenn Sie dies bestätigen, können die Messaging-Server Ihre IP-Adresse sowie Ihren Provider sehen und mit welchen Servern Sie sich verbinden.</string>
<string name="update_onion_hosts_settings_question">Einstellung für .onion-Hosts aktualisieren?</string>
<string name="network_use_onion_hosts">Verwende .onion-Hosts</string>
<string name="network_use_onion_hosts_prefer">Wenn verfügbar</string>
<string name="network_use_onion_hosts_no">Nein</string>
@@ -405,9 +404,6 @@
<string name="network_use_onion_hosts_no_desc">Onion-Hosts werden nicht verwendet.</string>
<string name="network_use_onion_hosts_required_desc">Für die Verbindung werden Onion-Hosts benötigt.
\nBitte beachten Sie: Ohne .onion-Adresse können Sie keine Verbindung mit den Servern herstellen.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Wenn Onion-Hosts verfügbar sind, werden sie verwendet.</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion-Hosts werden nicht verwendet.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Für die Verbindung werden Onion-Hosts benötigt.</string>
<string name="appearance_settings">Erscheinungsbild</string>
<!-- Address Items - UserAddressView.kt -->
<string name="create_address">Adresse erstellen</string>
@@ -826,7 +822,6 @@
<string name="network_option_protocol_timeout">Protokollzeitüberschreitung</string>
<string name="network_option_ping_interval">PING-Intervall</string>
<string name="network_option_enable_tcp_keep_alive">TCP-Keep-Alive aktivieren</string>
<string name="network_options_revert">Zurücksetzen</string>
<string name="network_options_save">Speichern</string>
<string name="update_network_settings_question">Netzwerkeinstellungen aktualisieren?</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">Die Aktualisierung der Einstellungen wird den Client wieder mit allen Servern verbinden.</string>
@@ -1732,7 +1727,7 @@
<string name="migrate_from_device_all_data_will_be_uploaded">Alle Ihre Kontakte, Unterhaltungen und Dateien werden sicher verschlüsselt und in Daten-Paketen auf die konfigurierten XTFP-Relais hochgeladen.</string>
<string name="migrate_from_device_archive_and_upload">Archivieren und Hochladen</string>
<string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Warnung</b>: Das Archiv wird gelöscht.]]></string>
<string name="migrate_from_device_check_connection_and_try_again">Überprüfen Sie Ihre Internet-Verbindung und probieren Sie es nochmals</string>
<string name="migrate_from_device_check_connection_and_try_again">Überprüfen Sie Ihre Internetverbindung und probieren Sie es nochmals</string>
<string name="migrate_from_device_archiving_database">Datenbank wird archiviert</string>
<string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Bitte beachten Sie</b>: Aus Sicherheitsgründen wird die Nachrichtenentschlüsselung Ihrer Verbindungen abgebrochen, wenn Sie die gleiche Datenbank auf zwei Geräten nutzen.]]></string>
<string name="migrate_from_device_cancel_migration">Migration abbrechen</string>
@@ -1850,15 +1845,15 @@
<string name="settings_section_title_profile_images">Profil-Bilder</string>
<string name="v5_7_shape_profile_images">Form der Profil-Bilder</string>
<string name="v5_7_shape_profile_images_descr">Quadratisch, kreisförmig oder irgendetwas dazwischen.</string>
<string name="snd_error_relay">Fehler auf dem Zielserver: %1$s</string>
<string name="snd_error_relay">Fehler auf dem Ziel-Server: %1$s</string>
<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 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
<string name="snd_error_proxy_relay">Weiterleitungs-Server: %1$s
\nFehler auf dem Ziel-Server: %2$s</string>
<string name="snd_error_proxy">Weiterleitungs-Server: %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 Netzwerkeinstellungen kompatibel.</string>
@@ -1877,10 +1872,10 @@
<string name="private_routing_show_message_status">Nachrichtenstatus anzeigen</string>
<string name="network_smp_proxy_fallback_allow_downgrade">Herabstufung erlauben</string>
<string name="network_smp_proxy_mode_always_description">Sie nutzen immer privates Routing.</string>
<string name="network_smp_proxy_fallback_prohibit_description">Nachrichten werden nicht direkt versendet, selbst wenn Ihr oder der Zielserver kein privates Routing unterstützt.</string>
<string name="network_smp_proxy_fallback_prohibit_description">Nachrichten werden nicht direkt versendet, selbst wenn Ihr oder der Ziel-Server kein privates Routing unterstützt.</string>
<string name="settings_section_title_private_message_routing">PRIVATES NACHRICHTEN-ROUTING</string>
<string name="network_smp_proxy_fallback_allow_protected_description">Nachrichten werden direkt versendet, wenn die IP-Adresse geschützt ist, und Ihr oder der Zielserver kein privates Routing unterstützt.</string>
<string name="network_smp_proxy_fallback_allow_description">Nachrichten werden direkt versendet, wenn Ihr oder der Zielserver kein privates Routing unterstützt.</string>
<string name="network_smp_proxy_fallback_allow_protected_description">Nachrichten werden direkt versendet, wenn die IP-Adresse geschützt ist, und Ihr oder der Ziel-Server kein privates Routing unterstützt.</string>
<string name="network_smp_proxy_fallback_allow_description">Nachrichten werden direkt versendet, wenn Ihr oder der Ziel-Server kein privates Routing unterstützt.</string>
<string name="private_routing_explanation">Zum Schutz Ihrer IP-Adresse, wird für die Nachrichten-Auslieferung privates Routing über Ihre konfigurierten SMP-Server genutzt.</string>
<string name="network_smp_proxy_mode_unprotected_description">Sie nutzen privates Routing mit unbekannten Servern, wenn Ihre IP-Adresse nicht geschützt ist.</string>
<string name="protect_ip_address">IP-Adresse schützen</string>
@@ -2057,7 +2052,7 @@
<string name="servers_info_uploaded">Hochgeladen</string>
<string name="servers_info_proxied_servers_section_footer">Sie sind nicht mit diesen Servern verbunden. Zur Auslieferung von Nachrichten an diese Server wird privates Routing genutzt.</string>
<string name="servers_info_subscriptions_total">Summe aller Abonnements</string>
<string name="servers_info_reset_stats_alert_message">Die Serverstatistiken werden zurückgesetzt. Dies kann nicht rückgängig gemacht werden!</string>
<string name="servers_info_reset_stats_alert_message">Die Server-Statistiken werden zurückgesetzt. Dies kann nicht rückgängig gemacht werden!</string>
<string name="size">Größe</string>
<string name="servers_info_starting_from">Beginnend mit %s.</string>
<string name="subscribed">Abonniert</string>
@@ -2071,15 +2066,55 @@
<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="smp_proxy_error_broker_host">Die Weiterleitungs-Server-Adresse ist nicht kompatibel mit den Netzwerkeinstellungen: %1$s.</string>
<string name="proxy_destination_error_failed_to_connect">Die Verbindung des Weiterleitungs-Servers %1$s zum Ziel-Server %2$s schlug fehl. Bitte versuchen Sie es später erneut.</string>
<string name="proxy_destination_error_broker_version">Die Ziel-Server-Version von %1$s ist nicht mit dem Weiterleitungs-Server %2$s kompatibel.</string>
<string name="smp_proxy_error_broker_version">Die Weiterleitungs-Server-Version ist nicht kompatibel mit den Netzwerkeinstellungen: %1$s.</string>
<string name="proxy_destination_error_broker_host">Die Ziel-Server-Adresse von %1$s ist nicht mit den Einstellungen des Weiterleitungs-Servers %2$s kompatibel.</string>
<string name="smp_proxy_error_connecting">Fehler beim Verbinden zum Weiterleitungs-Server %1$s. Bitte versuchen Sie es später erneut.</string>
<string name="privacy_media_blur_radius">Medium verpixeln</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_strong">Stark</string>
<string name="privacy_media_blur_radius_medium">Mittel</string>
<string name="privacy_media_blur_radius_off">Aus</string>
<string name="info_view_connect_button">Verbinden</string>
<string name="info_view_message_button">Nachricht</string>
<string name="info_view_open_button">Öffnen</string>
<string name="conversation_deleted">Unterhaltung gelöscht!</string>
<string name="only_delete_conversation">Nur die Unterhaltung löschen</string>
<string name="info_view_search_button">Suchen</string>
<string name="info_view_video_button">Video</string>
<string name="you_can_still_view_conversation_with_contact">Sie können die Unterhaltung mit %1$s weiterhin in der Chatliste einsehen.</string>
<string name="paste_link">Link einfügen</string>
<string name="deleted_chats">Gelöschte Chats</string>
<string name="no_filtered_contacts">Keine gefilterten Kontakte</string>
<string name="contact_list_header_title">Ihre Kontakte</string>
<string name="one_hand_ui">Einhand-Bedienoberfläche</string>
<string name="calls_prohibited_ask_to_enable_calls_alert_text">Bitten Sie Ihren Kontakt darum, Anrufe zu aktivieren.</string>
<string name="you_need_to_allow_calls">Sie müssen Ihrem Kontakt Anrufe zu Ihnen erlauben, bevor Sie ihn selbst anrufen können.</string>
<string name="allow_calls_question">Anrufe erlauben?</string>
<string name="info_view_call_button">Anrufen</string>
<string name="cant_call_contact_alert_title">Kontakt kann nicht angerufen werden</string>
<string name="calls_prohibited_alert_title">Anrufe nicht zugelassen!</string>
<string name="confirm_delete_contact_question">Löschen des Kontakts bestätigen?</string>
<string name="contact_deleted">Kontakt gelöscht!</string>
<string name="cant_call_member_alert_title">Gruppenmitglied kann nicht angerufen werden</string>
<string name="cant_send_message_to_member_alert_title">Nachricht an Gruppenmitglied nicht möglich</string>
<string name="cant_call_contact_connecting_wait_alert_text">Verbinde mit Kontakt, bitte warten oder später erneut überprüfen!</string>
<string name="cant_call_contact_deleted_alert_text">Kontakt wurde gelöscht.</string>
<string name="delete_contact_cannot_undo_warning">Kontakt wird gelöscht. Dies kann nicht rückgängig gemacht werden!</string>
<string name="delete_without_notification">Ohne Benachrichtigung löschen</string>
<string name="action_button_add_members">Einladen</string>
<string name="keep_conversation">Unterhaltung behalten</string>
<string name="cant_call_member_send_message_alert_text">Nachricht senden, um Anrufe zu aktivieren.</string>
<string name="you_can_still_send_messages_to_contact">Sie können weiterhin Nachrichten an %1$s von den gelöschten Chats aus senden.</string>
<string name="toolbar_settings">Einstellungen</string>
<string name="moderate_messages_will_be_deleted_warning">Die Nachrichten werden für alle Mitglieder gelöscht.</string>
<string name="moderate_messages_will_be_marked_warning">Die Nachrichten werden für alle Mitglieder als moderiert markiert.</string>
<string name="delete_members_messages__question">%d Nachrichten der Mitglieder löschen?</string>
<string name="compose_message_placeholder">Nachricht</string>
<string name="delete_messages_mark_deleted_warning">Nachrichten werden zur Löschung markiert. Der/Die Empfänger hat/haben die Möglichkeit, diese Nachrichten aufzudecken.</string>
<string name="selected_chat_items_selected_n">%d ausgewählt</string>
<string name="selected_chat_items_nothing_selected">Es wurde Nichts ausgewählt</string>
<string name="select_verb">Auswählen</string>
</resources>
@@ -447,7 +447,6 @@
<string name="ensure_smp_server_address_are_correct_format_and_unique">Asegúrate de que las direcciones del servidor SMP tienen el formato correcto, están separadas por líneas y no están duplicadas.</string>
<string name="icon_descr_instant_notifications">Notificación instantánea</string>
<string name="network_settings_title">Configuración de red</string>
<string name="network_use_onion_hosts_no_desc_in_alert">No se usarán hosts .onion</string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Sólo los dispositivos cliente almacenan perfiles de usuario, contactos, grupos y mensajes enviados con <b>cifrado de extremo a extremo de 2 capas</b> .]]></string>
<string name="onboarding_notifications_mode_subtitle">Puedes cambiar estos ajustes más tarde en Configuración.</string>
<string name="onboarding_notifications_mode_service">Instantánea</string>
@@ -466,7 +465,6 @@
<string name="notification_preview_mode_message">Contacto y texto</string>
<string name="member_info_section_title_member">MIEMBRO</string>
<string name="chat_item_ttl_none">nunca</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Se requieren hosts .onion para la conexión</string>
<string name="network_use_onion_hosts_no_desc">No se usarán hosts .onion</string>
<string name="settings_notification_preview_title">Vista previa de notificaciones</string>
<string name="alert_title_group_invitation_expired">¡Invitación caducada!</string>
@@ -511,7 +509,6 @@
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Asegúrate de que las direcciones del servidor WebRTC ICE tienen el formato correcto, están separadas por líneas y no duplicadas.</string>
<string name="network_use_onion_hosts_required_desc">Se requieren hosts .onion para la conexión
\nRecuerda: no podrás conectarte a servidores que no tengan dirección .onion.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Se usarán hosts .onion si están disponibles.</string>
<string name="immune_to_spam_and_abuse">Inmune a spam y abuso</string>
<string name="many_people_asked_how_can_it_deliver"><![CDATA[Muchos se preguntarán: <i>si SimpleX no tiene identificadores de usuario, ¿cómo puede entregar los mensajes\?</i>]]></string>
<string name="incoming_video_call">Videollamada entrante</string>
@@ -636,7 +633,6 @@
<string name="save_and_notify_contacts">Guardar y notificar contactos</string>
<string name="opensource_protocol_and_code_anybody_can_run_servers">Protocolo y código abiertos: cualquiera puede usar los servidores.</string>
<string name="role_in_group">Rol</string>
<string name="network_options_revert">Revertir</string>
<string name="network_option_ping_interval">Intervalo PING</string>
<string name="network_option_ping_count">Contador PING</string>
<string name="only_your_contact_can_delete">Sólo tu contacto puede eliminar mensajes de forma irreversible (tu puedes marcarlos para eliminar). (24 horas)</string>
@@ -800,7 +796,6 @@
<string name="star_on_github">Estrella en GitHub</string>
<string name="smp_servers_per_user">Lista de servidores para las conexiones nuevas de tu perfil actual</string>
<string name="network_disable_socks">¿Usar conexión directa a Internet\?</string>
<string name="update_onion_hosts_settings_question">¿Actualizar la configuración de los hosts .onion\?</string>
<string name="profile_is_only_shared_with_your_contacts">El perfil sólo se comparte con tus contactos.</string>
<string name="callstate_starting">inicializando…</string>
<string name="alert_title_skipped_messages">Mensajes omitidos</string>
@@ -588,7 +588,6 @@
<string name="network_use_onion_hosts_required_desc">میزبان‌های Onion برای اتصال الزامی خواهد بود.
\nلطفا توجه داشته باشید: شما بدون نشانی‌ onion. قادر نخواهید بود به سرورها متصل شوید.</string>
<string name="save_servers_button">ذخیره</string>
<string name="update_onion_hosts_settings_question">تنظیمات میزبان‌های onion. به روز شود؟</string>
<string name="port_verb">پورت</string>
<string name="network_enable_socks">از پروکسی SOCKS استفاده شود؟</string>
<string name="network_disable_socks">از اتصال مستقیم اینترنت استفاده شود؟</string>
@@ -597,7 +596,6 @@
<string name="host_verb">هاست</string>
<string name="network_use_onion_hosts_required">الزامی</string>
<string name="network_use_onion_hosts_prefer_desc">از میزبان‌های Onion وقتی موجود باشند استفاده خواهد شد.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">از میزبان‌های Onion وقتی موجود باشند استفاده خواهد شد.</string>
<string name="appearance_settings">ظاهر</string>
<string name="app_version_name">نسخه برنامه: v%s</string>
<string name="show_developer_options">نمایش گزینه‌های توسعه‌دهنده</string>
@@ -608,7 +606,6 @@
<string name="network_session_mode_user_description"><![CDATA[یک اتصال جدای TCP (و اطلاعات ورود SOCKS) <b>برای هر نمایه گپی که در برنامه دارید</b> استفاده خواهد شد.]]></string>
<string name="show_internal_errors">نمایش خطاهای داخلی</string>
<string name="show_slow_api_calls">نمایش تماس‌های کند API</string>
<string name="network_use_onion_hosts_no_desc_in_alert">از میزبان‌های Onion استفاده نخواهد شد.</string>
<string name="network_use_onion_hosts_no">خیر</string>
<string name="disable_onion_hosts_when_not_supported"><![CDATA[<i>استفاده از میزبان‌های onion.</i> را روی «خیر» تنظیم کنید اگر پروکسی SOCKS از آنها پشتیبانی نمی‌کند.]]></string>
<string name="customize_theme_title">سفارشی کردن تم</string>
@@ -630,7 +627,6 @@
<string name="network_session_mode_entity">اتصال</string>
<string name="app_version_code">ساختار برنامه: %s</string>
<string name="network_settings_title">تنظیمات شبکه</string>
<string name="network_use_onion_hosts_required_desc_in_alert">میزبان‌های Onion برای اتصال الزامی خواهد بود.</string>
<string name="show_dev_options">نمایش:</string>
<string name="socks_proxy_setting_limitations"><![CDATA[<b>لطفا توجه داشته باشید</b>: واسطه‌های پیام و پرونده از طریق پروکسی SOCKS متصل می‌شوند. تماس‌ها و ارسال پیش‌نمایش‌های لینک از اتصال مستقیم استفاده می‌کنند.]]></string>
<string name="developer_options_section">گزینه‌های توسعه‌دهنده</string>
@@ -1313,7 +1309,6 @@
<string name="network_option_ping_interval">وقفه پینگ</string>
<string name="network_option_ping_count">شمار پینگ</string>
<string name="network_option_enable_tcp_keep_alive">فعال کردن زنده نگه‌داشتن TCP</string>
<string name="network_options_revert">برگشت</string>
<string name="network_options_save">ذخیره</string>
<string name="update_network_settings_question">تنظیمات شبکه به‌روزرسانی شود؟</string>
<string name="users_add">افزودن نمایه</string>
@@ -623,7 +623,6 @@
<string name="text_field_set_contact_placeholder">Aseta kontaktin nimi…</string>
<string name="save_passphrase_in_keychain">Tallenna salasana Keystoreen</string>
<string name="only_your_contact_can_send_disappearing">Vain kontaktisi voi lähettää katoavia viestejä.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion-isäntiä käytetään, kun niitä on saatavilla.</string>
<string name="port_verb">Portti</string>
<string name="network_proxy_port">portti %d</string>
<string name="disable_onion_hosts_when_not_supported"><![CDATA[Aseta <i>Use .onion hosts</i> arvoon Ei, jos SOCKS-välityspalvelin ei tue niitä.]]></string>
@@ -661,7 +660,6 @@
<string name="network_option_tcp_connection_timeout">TCP-yhteyden aikakatkaisu</string>
<string name="network_option_ping_count">PING-määrä</string>
<string name="network_option_ping_interval">PING-väli</string>
<string name="network_options_revert">Palauta</string>
<string name="users_delete_with_connections">Profiili- ja palvelinyhteydet</string>
<string name="set_group_preferences">Aseta ryhmän asetukset</string>
<string name="many_people_asked_how_can_it_deliver"><![CDATA[Monet ovat kysyneet: <i>jos SimpleX ei sisällä käyttäjätunnuksia, kuinka se voi toimittaa viestejä\?</i>]]></string>
@@ -916,8 +914,6 @@
<string name="thank_you_for_installing_simplex">Kiitos SimpleX Chatin asentamisesta!</string>
<string name="icon_descr_settings">Asetukset</string>
<string name="icon_descr_more_button">Lisää</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion-isäntiä ei käytetä.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Yhteyden muodostamiseen tarvitaan Onion-isäntiä.</string>
<string name="onboarding_notifications_mode_title">Yksityiset ilmoitukset</string>
<string name="icon_descr_speaker_off">Kaiutin pois päältä</string>
<string name="icon_descr_speaker_on">Kaiutin päällä</string>
@@ -1156,7 +1152,6 @@
\nkontakteillesi</string>
<string name="your_ICE_servers">ICE-palvelimesi</string>
<string name="network_use_onion_hosts_prefer">Kun saatavilla</string>
<string name="update_onion_hosts_settings_question">Päivitä .onion-isäntien asetus\?</string>
<string name="network_use_onion_hosts">Käytä .onion-isäntiä</string>
<string name="your_calls">Puhelusi</string>
<string name="alert_message_no_group">Tätä ryhmää ei enää ole olemassa.</string>
@@ -321,8 +321,6 @@
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Assurez-vous que les adresses des serveurs WebRTC ICE sont au bon format et ne sont pas dupliquées, un par ligne.</string>
<string name="network_enable_socks_info">Accéder aux serveurs via un proxy SOCKS sur le port %d \? Le proxy doit être démarré avant d\'activer cette option.</string>
<string name="network_use_onion_hosts">Utiliser les hôtes .onions</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Les hôtes .onion seront utilisés lorsqu\'ils sont disponibles.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Les hôtes .onion seront nécessaires pour la connexion.</string>
<string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[Vous contrôlez par quel·s serveur·s vous pouvez <b>transmettre</b> ainsi que par quel·s serveur·s vous pouvez <b>recevoir</b> les messages de vos contacts.]]></string>
<string name="your_settings">Vos paramètres</string>
<string name="chat_lock">SimpleX Lock</string>
@@ -414,12 +412,10 @@
<string name="how_to">Comment faire</string>
<string name="enter_one_ICE_server_per_line">Serveurs ICE (un par ligne)</string>
<string name="error_saving_ICE_servers">Erreur lors de la sauvegarde des serveurs ICE</string>
<string name="update_onion_hosts_settings_question">Mettre à jour le paramètre des hôtes .onion \?</string>
<string name="network_use_onion_hosts_prefer">Quand disponible</string>
<string name="network_use_onion_hosts_no_desc">Les hôtes .onion ne seront pas utilisés.</string>
<string name="network_use_onion_hosts_required_desc">Les hôtes .onion seront nécessaires pour la connexion.
\nAttention : vous ne pourrez pas vous connecter aux serveurs sans adresse .onion.</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Les hôtes .onion ne seront pas utilisés.</string>
<string name="delete_address__question">Supprimer l\'adresse \?</string>
<string name="all_your_contacts_will_remain_connected">Tous vos contacts resteront connectés.</string>
<string name="share_link">Partager le lien</string>
@@ -833,7 +829,6 @@
<string name="conn_level_desc_direct">directe</string>
<string name="group_is_decentralized">Entièrement décentralisé – visible que par ses membres.</string>
<string name="group_members_can_send_disappearing">Les membres du groupes peuvent envoyer des messages éphémères.</string>
<string name="network_options_revert">Revenir en arrière</string>
<string name="prohibit_sending_disappearing_messages">Interdire l’envoi de messages éphémères.</string>
<string name="incognito_info_protects">Le mode incognito protège votre vie privée en utilisant un nouveau profil aléatoire pour chaque contact.</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">La mise à jour des ces paramètres reconnectera le client à tous les serveurs.</string>
@@ -1989,4 +1984,47 @@
<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>
<string name="privacy_media_blur_radius_medium">Modéré</string>
<string name="privacy_media_blur_radius">Flouter les médias</string>
<string name="proxy_destination_error_broker_host">L\'adresse du serveur de destination %1$s est incompatible avec les paramètres du serveur de redirection %2$s.</string>
<string name="proxy_destination_error_broker_version">La version du serveur de destination %1$s est incompatible avec le serveur de redirection %2$s.</string>
<string name="proxy_destination_error_failed_to_connect">Le serveur de redirection %1$s n\'a pas réussi à se connecter au serveur de destination %2$s. Veuillez réessayer plus tard.</string>
<string name="smp_proxy_error_broker_host">L\'adresse du serveur de redirection est incompatible avec les paramètres du réseau : %1$s.</string>
<string name="smp_proxy_error_broker_version">La version du serveur de redirection est incompatible avec les paramètres du réseau : %1$s.</string>
<string name="privacy_media_blur_radius_strong">Fort</string>
<string name="smp_proxy_error_connecting">Erreur de connexion au serveur de redirection %1$s. Veuillez réessayer plus tard.</string>
<string name="privacy_media_blur_radius_off">Off</string>
<string name="privacy_media_blur_radius_soft">Léger</string>
<string name="toolbar_settings">Paramètres</string>
<string name="info_view_connect_button">se connecter</string>
<string name="info_view_message_button">message</string>
<string name="info_view_open_button">ouvrir</string>
<string name="confirm_delete_contact_question">Confirmer la suppression du contact ?</string>
<string name="delete_contact_cannot_undo_warning">Le contact sera supprimé - il n\'est pas possible de revenir en arrière !</string>
<string name="delete_without_notification">Supprimer sans notification</string>
<string name="keep_conversation">Garder la conversation</string>
<string name="only_delete_conversation">Ne supprimer que la conversation</string>
<string name="info_view_search_button">rechercher</string>
<string name="info_view_video_button">vidéo</string>
<string name="contact_deleted">Contact supprimé !</string>
<string name="you_can_still_send_messages_to_contact">Vous pouvez toujours envoyer des messages à %1$s à partir des chats supprimés.</string>
<string name="deleted_chats">Discussions supprimées</string>
<string name="no_filtered_contacts">Pas de contacts filtrés</string>
<string name="paste_link">Coller le lien</string>
<string name="contact_list_header_title">Vos contacts</string>
<string name="one_hand_ui">Interface utilisateur à une main</string>
<string name="cant_call_contact_deleted_alert_text">Le contact est supprimé.</string>
<string name="calls_prohibited_alert_title">Les appels ne sont pas autorisés !</string>
<string name="you_need_to_allow_calls">Vous devez autoriser votre contact à appeler pour pouvoir l\'appeler.</string>
<string name="cant_send_message_to_member_alert_title">Impossible d\'envoyer un message à ce membre du groupe</string>
<string name="you_can_still_view_conversation_with_contact">Vous pouvez toujours consulter la conversation avec %1$s dans la liste des conversation.</string>
<string name="allow_calls_question">Autoriser les appels ?</string>
<string name="info_view_call_button">appel</string>
<string name="cant_call_contact_alert_title">Impossible d\'appeler le contact</string>
<string name="cant_call_contact_connecting_wait_alert_text">Connexion au contact, veuillez patienter ou vérifier plus tard !</string>
<string name="cant_call_member_alert_title">Impossible d\'appeler ce membre du groupe</string>
<string name="conversation_deleted">Conversation supprimée !</string>
<string name="action_button_add_members">Inviter</string>
<string name="calls_prohibited_ask_to_enable_calls_alert_text">Veuillez demander à votre contact d\'autoriser les appels.</string>
<string name="cant_call_member_send_message_alert_text">Envoyer un message pour activer les appels.</string>
</resources>
@@ -299,7 +299,7 @@
<string name="group_member_status_accepted">kapcsolódás (elfogadva)</string>
<string name="smp_servers_check_address">Kiszolgáló címének ellenőrzése és újrapróbálkozás.</string>
<string name="delete_group_question">Csoport törlése?</string>
<string name="confirm_database_upgrades">Adatbázis frissítés megerősítése</string>
<string name="confirm_database_upgrades">Adatbázis fejlesztésének megerősítése</string>
<string name="create_your_profile">Saját profil létrehozása</string>
<string name="snd_conn_event_switch_queue_phase_changing">cím megváltoztatása…</string>
<string name="display_name_connecting">kapcsolódás…</string>
@@ -349,7 +349,7 @@
<string name="delete_pending_connection__question">Függő kapcsolatfelvételi kérések törlése?</string>
<string name="database_encrypted">Adatbázis titkosítva!</string>
<string name="clear_chat_question">Üzenetek kiürítése?</string>
<string name="database_downgrade">Visszatérés a korábbi adatbázis verzióra</string>
<string name="database_downgrade">Adatbázis visszafejlesztése</string>
<string name="clear_chat_button">Üzenetek kiürítése</string>
<string name="database_passphrase_will_be_updated">Adatbázis titkosítási jelmondat frissítve lesz.</string>
<string name="multicast_connect_automatically">Kapcsolódás automatikusan</string>
@@ -381,7 +381,7 @@
<string name="ttl_months">%d hónap</string>
<string name="delete_address__question">Cím törlése?</string>
<string name="receipts_contacts_title_disable">Üzenet kézbesítési jelentések letiltása?</string>
<string name="passphrase_is_different">Az adatbázis jelmondat eltérő a Keystore-ba elmentettől.</string>
<string name="passphrase_is_different">Az adatbázis jelmondata eltér a Keystore-ban lévőtől.</string>
<string name="direct_messages">Közvetlen üzenetek</string>
<string name="icon_descr_email">E-mail</string>
<string name="receipts_contacts_disable_for_all">Letiltás mindenki számára</string>
@@ -460,7 +460,7 @@
<string name="desktop_app_version_is_incompatible">Számítógép kliens verziója %s nem kompatibilis ezzel az alkalmazással.</string>
<string name="delivery">Kézbesítés</string>
<string name="total_files_count_and_size">%d fájl %s összméretben</string>
<string name="database_passphrase_is_required">Adatbázis jelmondat szükséges a csevegés megnyitásához.</string>
<string name="database_passphrase_is_required">A csevegés megnyitásához adja meg az adatbázis jelmondatát.</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">A kézbesítési jelentések le vannak tiltva!</string>
@@ -508,7 +508,7 @@
<string name="encrypt_database">Titkosít</string>
<string name="alert_title_no_group">Csoport nem található!</string>
<string name="error_saving_smp_servers">Hiba az SMP kiszolgálók mentésekor</string>
<string name="downgrade_and_open_chat">Visszatérés a korábbi verzióra és a csevegés megnyitása</string>
<string name="downgrade_and_open_chat">Visszafejlesztés és a csevegés megnyitása</string>
<string name="icon_descr_group_inactive">A csoport inaktív</string>
<string name="v5_0_large_files_support_descr">Gyors és nem kell várni, amíg a feladó online lesz!</string>
<string name="error_joining_group">Hiba a csoporthoz való csatlakozáskor</string>
@@ -740,9 +740,7 @@
<string name="new_member_role">Új tag szerepköre</string>
<string name="la_mode_off">Kikapcsolva</string>
<string name="invalid_contact_link">Érvénytelen hivatkozás!</string>
<string name="network_use_onion_hosts_required_desc_in_alert">A kapcsolódáshoz Onion kiszolgálókra lesz szükség.</string>
<string name="new_in_version">Változások a %s verzióban</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion kiszolgálók használata, ha azok rendelkezésre állnak.</string>
<string name="smp_servers_invalid_address">Érvénytelen kiszolgálócím!</string>
<string name="thousand_abbreviation">k</string>
<string name="chat_item_ttl_none">soha</string>
@@ -765,7 +763,6 @@
<string name="chat_preferences_on">bekapcsolva</string>
<string name="v5_1_japanese_portuguese_interface">Japán és portugál kezelőfelület</string>
<string name="message_deletion_prohibited_in_chat">Az üzenetek végleges törlése le van tiltva ebben a csoportban.</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion kiszolgálók nem lesznek használva.</string>
<string name="remote_host_was_disconnected_toast"><![CDATA[A(z) <b>%s</b> eszközzel megszakadt a kapcsolat]]></string>
<string name="custom_time_unit_months">hónap</string>
<string name="privacy_message_draft">Üzenetvázlat</string>
@@ -1079,7 +1076,7 @@
<string name="your_current_profile">Jelenlegi profil</string>
<string name="smp_server_test_upload_file">Fájl feltöltése</string>
<string name="prohibit_calls">A hívások kezdeményezése le van tiltva.</string>
<string name="network_use_onion_hosts_required">Megkövetelt</string>
<string name="network_use_onion_hosts_required">Szükséges</string>
<string name="ntf_channel_messages">SimpleX Chat üzenetek</string>
<string name="restore_database_alert_confirm">Visszaállítás</string>
<string name="setup_database_passphrase">Adatbázis jelmondat beállítása</string>
@@ -1098,7 +1095,6 @@
<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á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>
@@ -1122,7 +1118,7 @@
<string name="view_security_code">Biztonsági kód megtekintése</string>
<string name="unblock_member_question">Tag feloldása?</string>
<string name="sender_may_have_deleted_the_connection_request">A küldő törölhette a kapcsolódási kérelmet.</string>
<string name="wrong_passphrase">Téves adatbázis jelmondat</string>
<string name="wrong_passphrase">Hibás adatbázis jelmondat</string>
<string name="your_SMP_servers">SMP kiszolgálók</string>
<string name="send_receipts_disabled_alert_title">Az üzenet kézbesítési jelentések le vannak tiltva</string>
<string name="open_database_folder">Adatbázis mappa megnyitása</string>
@@ -1169,7 +1165,7 @@
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
<string name="unlink_desktop">Szétkapcsolás</string>
<string name="incognito_random_profile">Véletlenszerű profil</string>
<string name="wrong_passphrase_title">Téves jelmondat!</string>
<string name="wrong_passphrase_title">Hibás jelmondat!</string>
<string name="prohibit_message_reactions_group">Az üzenetreakciók küldése le van tiltva.</string>
<string name="language_system">Rendszer</string>
<string name="icon_descr_received_msg_status_unread">olvasatlan</string>
@@ -1405,9 +1401,9 @@
<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 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 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_can_enable_delivery_receipts_later_alert">Később engedélyezheti őket az alkalmazás „Adatvédelem és biztonság” menüjében.</string>
<string name="to_reveal_profile_enter_password">Rejtett profilja felfedé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">Fejlesztés és a csevegé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>
<string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Már a(z) <b>%1$s</b> csoport tagja.]]></string>
@@ -1432,7 +1428,6 @@
<string name="unknown_database_error_with_info">Ismeretlen adatbázis hiba: %s</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>
<string name="you_joined_this_group">Csatlakozott ehhez a csoporthoz</string>
<string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Ez a hivatkozása a(z) <b>%1$s</b> csoporthoz!]]></string>
@@ -1476,7 +1471,7 @@
<string name="v5_0_app_passcode_descr">Rendszerhitelesítés helyetti beállítás.</string>
<string name="switch_receiving_address_desc">A fogadó cím egy másik kiszolgálóra változik. A címváltoztatás a feladó online állapotba kerülése után fejeződik be.</string>
<string name="stop_chat_to_export_import_or_delete_chat_database">A csevegés megállítása a csevegő adatbázis exportálásához, importálásához, vagy törléséhez. A csevegés megállítása alatt nem tud üzeneteket fogadni és küldeni.</string>
<string name="save_passphrase_in_keychain">Jelmondat mentése a kulcstárolóba</string>
<string name="save_passphrase_in_keychain">Jelmondat mentése a Keystore-ba</string>
<string name="v4_6_chinese_spanish_interface_descr">Köszönet a felhasználóknak - hozzájárulás a Weblaten!</string>
<string name="save_passphrase_in_settings">Jelmondat mentése a beállításokban</string>
<string name="send_receipts_disabled_alert_msg">Ennek a csoportnak több mint %1$d tagja van, a kézbesítési jelentések nem kerülnek elküldésre.</string>
@@ -1768,7 +1763,7 @@
\nHiba: %2$s</string>
<string name="snd_error_expired">Hálózati problémák - az üzenet többszöri elküldési kísérlet után lejárt.</string>
<string name="srv_error_version">A kiszolgáló verziója nem kompatibilis a hálózati beállításokkal.</string>
<string name="snd_error_auth">Rossz kulcs vagy ismeretlen kapcsolat - valószínűleg ez a kapcsolat törlődött.</string>
<string name="snd_error_auth">Hibás kulcs vagy ismeretlen kapcsolat - valószínűleg ez a kapcsolat törlődött.</string>
<string name="snd_error_proxy_relay">Továbbító kiszolgáló: %1$s
\nCélkiszolgáló hiba: %2$s</string>
<string name="ci_status_other_error">Hiba: %1$s</string>
@@ -1779,7 +1774,7 @@
<string name="network_smp_proxy_mode_unknown">Ismeretlen átjátszók</string>
<string name="network_smp_proxy_fallback_allow_protected">Ha az IP-cím rejtett</string>
<string name="private_routing_show_message_status">Üzenet állapot megjelenítése</string>
<string name="network_smp_proxy_fallback_allow_downgrade">Korábbi verzióra történő visszatérés engedélyezése</string>
<string name="network_smp_proxy_fallback_allow_downgrade">Visszafejlesztés engedélyezése</string>
<string name="network_smp_proxy_mode_always">Mindig</string>
<string name="network_smp_proxy_fallback_prohibit">Nem</string>
<string name="network_smp_proxy_mode_unprotected">Nem védett</string>
@@ -1983,4 +1978,55 @@
<string name="app_check_for_updates_notice_disable">Letiltás</string>
<string name="app_check_for_updates_disabled">Letiltva</string>
<string name="app_check_for_updates_stable">Stabil</string>
<string name="smp_proxy_error_connecting">Hiba a(z) %1$s továbbító kiszolgálóhoz való kapcsolódáskor. Próbálja meg később.</string>
<string name="proxy_destination_error_broker_version">A(z) %1$s célkiszolgáló verziója nem kompatibilis a(z) %2$s továbbító kiszolgálóval.</string>
<string name="proxy_destination_error_failed_to_connect">A(z) %1$s továbbító kiszolgáló nem tudott csatlakozni a(z) %2$s célkiszolgálóhoz. Próbálja meg később.</string>
<string name="proxy_destination_error_broker_host">A(z) %1$s célkiszolgáló címe nem kompatibilis a(z) %2$s továbbító kiszolgáló beállításaival.</string>
<string name="privacy_media_blur_radius">Média elhomályosítása</string>
<string name="privacy_media_blur_radius_medium">Közepes</string>
<string name="privacy_media_blur_radius_off">Kikapcsolva</string>
<string name="privacy_media_blur_radius_soft">Enyhe</string>
<string name="privacy_media_blur_radius_strong">Erős</string>
<string name="smp_proxy_error_broker_host">A továbbító kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %1$s.</string>
<string name="smp_proxy_error_broker_version">A továbbító kiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %1$s.</string>
<string name="info_view_call_button">hívás</string>
<string name="delete_contact_cannot_undo_warning">Az ismerős törlésre fog kerülni - ez a művelet nem vonható vissza!</string>
<string name="only_delete_conversation">Csak a beszélgetés törlése</string>
<string name="info_view_open_button">megnyitás</string>
<string name="conversation_deleted">Beszélgetés törölve!</string>
<string name="contact_deleted">Ismerős törölve!</string>
<string name="deleted_chats">Törölt csevegések</string>
<string name="no_filtered_contacts">Nincsenek szűrt ismerősök</string>
<string name="paste_link">Hivatkozás beillesztése</string>
<string name="calls_prohibited_alert_title">A hívások le vannak tiltva!</string>
<string name="cant_call_contact_alert_title">Nem lehet felhívni az ismerőst</string>
<string name="cant_send_message_to_member_alert_title">Nem lehet üzenetet küldeni a csoporttagnak</string>
<string name="cant_call_contact_connecting_wait_alert_text">Kapcsolódás az ismerőshöz, várjon vagy ellenőrizze később!</string>
<string name="cant_call_contact_deleted_alert_text">Törölt ismerős.</string>
<string name="cant_call_member_alert_title">Nem lehet felhívni a csoporttagot</string>
<string name="allow_calls_question">Hívások engedélyezése?</string>
<string name="action_button_add_members">Meghívás</string>
<string name="info_view_message_button">üzenet</string>
<string name="keep_conversation">Beszélgetés megtartása</string>
<string name="confirm_delete_contact_question">Biztosan törli az ismerőst?</string>
<string name="info_view_connect_button">kapcsolódás</string>
<string name="one_hand_ui">Egykezes UI</string>
<string name="delete_without_notification">Törlés értesítés nélkül</string>
<string name="toolbar_settings">Beállítások</string>
<string name="info_view_search_button">keresés</string>
<string name="info_view_video_button">videó</string>
<string name="you_can_still_send_messages_to_contact">A „Törölt csevegésekből” továbbra is küldhet üzeneteket neki: %1$s.</string>
<string name="contact_list_header_title">Ismerősök</string>
<string name="calls_prohibited_ask_to_enable_calls_alert_text">Kérje meg az ismerősét, hogy engedélyezze a hívásokat.</string>
<string name="cant_call_member_send_message_alert_text">Üzenet küldése a hívások engedélyezéséhez.</string>
<string name="you_need_to_allow_calls">Engedélyeznie kell a hívásokat az ismerőse számára, hogy fel tudják hívni egymást.</string>
<string name="you_can_still_view_conversation_with_contact">A(z) %1$s nevű ismerősével folytatott beszélgetéseit továbbra is megtekintheti a csevegések listájában.</string>
<string name="compose_message_placeholder">Üzenet</string>
<string name="select_verb">Kiválaszt</string>
<string name="moderate_messages_will_be_marked_warning">Az üzenetek minden tag számára moderáltként lesznek megjelölve.</string>
<string name="selected_chat_items_nothing_selected">Nincs kiválasztva semmi</string>
<string name="delete_messages_mark_deleted_warning">Az üzenetek törlésre lesznek jelölve. A címzett(ek) képes(ek) lesz(nek) felfedni ezt az üzenetet.</string>
<string name="delete_members_messages__question">Törli a tagok %d üzenetét?</string>
<string name="selected_chat_items_selected_n">%d kiválasztva</string>
<string name="moderate_messages_will_be_deleted_warning">Az üzenetek minden tag számára törlésre kerülnek.</string>
</resources>
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M421.5-300.5 701-580l-44.5-43.5-235 235-119-119L259-464l162.5 163.5ZM480.06-85q-80.97 0-153.13-31.26-72.15-31.27-125.79-85Q147.5-255 116.25-327.02 85-399.05 85-479.94q0-81.97 31.26-154.13 31.27-72.15 85-125.54Q255-813 327.02-844q72.03-31 152.92-31 81.97 0 154.13 31.13 72.17 31.13 125.55 84.5Q813-706 844-633.98q31 72.03 31 153.92 0 80.97-31.01 153.13-31.02 72.15-84.5 125.79Q706-147.5 633.98-116.25 561.95-85 480.06-85Zm-.09-57.5q140.53 0 239.03-98.97 98.5-98.96 98.5-238.5 0-140.53-98.47-239.03-98.46-98.5-239-98.5-139.53 0-238.53 98.47-99 98.46-99 239 0 139.53 98.97 238.53 98.96 99 238.5 99ZM480-480Z"/></svg>

After

Width:  |  Height:  |  Size: 729 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000">
<path
d="M730-455q12.25 0 20.63-8.43 8.37-8.42 8.37-20.5 0-12.07-8.37-20.32-8.38-8.25-20.63-8.25H605q-12.25 0-20.62 8.46-8.38 8.47-8.38 20.22 0 12.32 8.38 20.57Q592.75-455 605-455h125Zm0-116.5q12.25 0 20.63-8.18 8.37-8.17 8.37-20.5 0-12.32-8.37-20.57Q742.25-629 730-629H605q-12.25 0-20.62 8.43-8.38 8.42-8.38 20.75 0 12.32 8.38 20.32 8.37 8 20.62 8h125Zm-370.25 136q-38.63 0-66.58 6.5-27.94 6.5-49.17 19.5-20 11-31 28.61T202-343q0 9.5 6.39 15.5 6.38 6 15.61 6h270.5q9.23 0 15.61-6.23 6.39-6.24 6.39-15.77 0-18.69-10.75-36.1Q495-397 476-409.5q-21.5-13.5-50-19.75t-66.25-6.25Zm-.58-60q29.59 0 50.71-20.96Q431-537.41 431-566.84q0-29.42-20.98-50.54-20.98-21.12-50.44-21.12-29.84 0-50.71 20.96Q288-596.59 288-567.16q0 29.42 20.79 50.54 20.79 21.12 50.38 21.12Zm-216.67 331q-22.97 0-40.23-17.27Q85-199.03 85-222v-516q0-22.97 17.27-40.23 17.26-17.27 40.23-17.27h675q22.97 0 40.23 17.27Q875-760.97 875-738v516q0 22.97-17.27 40.23-17.26 17.27-40.23 17.27h-675Zm0-57.5h675v-516h-675v516Zm0 0v-516 516Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M480.06-85q-80.97 0-153.13-31.26-72.15-31.27-125.79-85Q147.5-255 116.25-327.02 85-399.05 85-479.94q0-81.97 31.26-154.13 31.27-72.15 85-125.54Q255-813 327.02-844q72.03-31 152.92-31 81.97 0 154.13 31.13 72.17 31.13 125.55 84.5Q813-706 844-633.98q31 72.03 31 153.92 0 80.97-31.01 153.13-31.02 72.15-84.5 125.79Q706-147.5 633.98-116.25 561.95-85 480.06-85Zm-.09-57.5q140.53 0 239.03-98.97 98.5-98.96 98.5-238.5 0-140.53-98.47-239.03-98.46-98.5-239-98.5-139.53 0-238.53 98.47-99 98.46-99 239 0 139.53 98.97 238.53 98.96 99 238.5 99ZM480-480Z"/></svg>

After

Width:  |  Height:  |  Size: 661 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000">
<path
d="M269.5-240.5h421q11.67 0 20.09-8.43 8.41-8.42 8.41-20.5 0-12.07-8.41-20.32-8.42-8.25-20.09-8.25h-421q-12.25 0-20.62 8.46-8.38 8.47-8.38 20.22 0 12.32 8.38 20.57 8.37 8.25 20.62 8.25Zm-87.5 116q-22.97 0-40.23-17.27Q124.5-159.03 124.5-182v-596q0-22.97 17.27-40.23Q159.03-835.5 182-835.5h596q22.97 0 40.23 17.27Q835.5-800.97 835.5-778v596q0 22.97-17.27 40.23Q800.97-124.5 778-124.5H182Zm0-57.5h596v-596H182v596Zm0-596v596-596Z" />
</svg>

After

Width:  |  Height:  |  Size: 563 B

@@ -610,18 +610,14 @@
<string name="network_use_onion_hosts_no">No</string>
<string name="network_use_onion_hosts_required_desc">Gli host Onion saranno necessari per la connessione.
\nNota bene: non potrai connetterti ai server senza indirizzo .onion .</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Gli host Onion saranno necessari per la connessione.</string>
<string name="network_use_onion_hosts_prefer_desc">Gli host Onion verranno usati quando disponibili.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Gli host Onion verranno usati quando disponibili.</string>
<string name="network_use_onion_hosts_no_desc">Gli host Onion non verranno usati.</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Gli host Onion non verranno usati.</string>
<string name="rate_the_app">Valuta l\'app</string>
<string name="network_use_onion_hosts_required">Obbligatorio</string>
<string name="save_servers_button">Salva</string>
<string name="saved_ICE_servers_will_be_removed">I server WebRTC ICE salvati verranno rimossi.</string>
<string name="share_link">Condividi link</string>
<string name="star_on_github">Dai una stella su GitHub</string>
<string name="update_onion_hosts_settings_question">Aggiornare l\'impostazione degli host .onion\?</string>
<string name="network_disable_socks">Usare una connessione internet diretta\?</string>
<string name="network_use_onion_hosts">Usa gli host .onion</string>
<string name="network_enable_socks">Usare il proxy SOCKS\?</string>
@@ -816,7 +812,6 @@
<string name="network_option_protocol_timeout">Scadenza del protocollo</string>
<string name="receiving_via">Ricezione via</string>
<string name="network_options_reset_to_defaults">Ripristina i predefiniti</string>
<string name="network_options_revert">Ripristina</string>
<string name="network_options_save">Salva</string>
<string name="save_group_profile">Salva il profilo del gruppo</string>
<string name="network_option_seconds_label">sec</string>
@@ -2000,4 +1995,44 @@
<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>
<string name="info_view_call_button">chiama</string>
<string name="info_view_message_button">messaggio</string>
<string name="info_view_open_button">apri</string>
<string name="info_view_search_button">cerca</string>
<string name="toolbar_settings">Impostazioni</string>
<string name="delete_without_notification">Elimina senza avvisare</string>
<string name="keep_conversation">Tieni la conversazione</string>
<string name="only_delete_conversation">Elimina solo la conversazione</string>
<string name="you_can_still_send_messages_to_contact">Puoi ancora inviare messaggi a %1$s dalle chat eliminate.</string>
<string name="deleted_chats">Chat eliminate</string>
<string name="no_filtered_contacts">Nessun contatto filtrato</string>
<string name="paste_link">Incolla link</string>
<string name="contact_list_header_title">I tuoi contatti</string>
<string name="one_hand_ui">Interfaccia a una mano</string>
<string name="action_button_add_members">Invita</string>
<string name="allow_calls_question">Consentire le chiamate?</string>
<string name="calls_prohibited_alert_title">Chiamate proibite!</string>
<string name="cant_call_contact_alert_title">Impossibile chiamare il contatto</string>
<string name="cant_call_member_alert_title">Impossibile chiamare il membro del gruppo</string>
<string name="cant_call_contact_connecting_wait_alert_text">In collegamento con il contatto, attendi o controlla più tardi!</string>
<string name="cant_call_contact_deleted_alert_text">Il contatto è stato eliminato.</string>
<string name="calls_prohibited_ask_to_enable_calls_alert_text">Chiedi al contatto di attivare le chiamate.</string>
<string name="you_need_to_allow_calls">Devi consentire le chiamate al tuo contatto per poterlo chiamare.</string>
<string name="cant_send_message_to_member_alert_title">Impossibile inviare messaggi al membro del gruppo</string>
<string name="delete_contact_cannot_undo_warning">Il contatto verrà eliminato - non è reversibile!</string>
<string name="conversation_deleted">Conversazione eliminata!</string>
<string name="cant_call_member_send_message_alert_text">Invia un messaggio per attivare le chiamate.</string>
<string name="info_view_video_button">video</string>
<string name="you_can_still_view_conversation_with_contact">Puoi ancora vedere la conversazione con %1$s nell\'elenco delle chat.</string>
<string name="info_view_connect_button">connetti</string>
<string name="contact_deleted">Contatto eliminato!</string>
<string name="confirm_delete_contact_question">Confermare l\'eliminazione del contatto?</string>
<string name="compose_message_placeholder">Messaggio</string>
<string name="selected_chat_items_nothing_selected">Nessuna selezione</string>
<string name="select_verb">Seleziona</string>
<string name="selected_chat_items_selected_n">Selezionato %d</string>
<string name="moderate_messages_will_be_deleted_warning">I messaggi verranno eliminati per tutti i membri.</string>
<string name="moderate_messages_will_be_marked_warning">I messaggi verranno contrassegnati come moderati per tutti i membri.</string>
<string name="delete_members_messages__question">Eliminare %d messaggi dei membri?</string>
<string name="delete_messages_mark_deleted_warning">I messaggi saranno contrassegnati per l\'eliminazione. Il/I destinatario/i sarà/saranno in grado di rivelare questi messaggi.</string>
</resources>
@@ -604,8 +604,6 @@
<string name="theme_light">בהיר</string>
<string name="import_theme_error_desc">ודאו שלקובץ יש תחביר YAML תקין. ייצאו ערכת נושא כדי לקבל דוגמה למבנה תקין של קובץ ערכת נושא.</string>
<string name="message_delivery_error_desc">ככל הנראה איש קשר זה מחק את החיבור איתך.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">ייעשה שימוש במארחי Onion כאשר יהיו זמינים.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">מארחי Onion יידרשו לחיבור.</string>
<string name="moderated_item_description">נחסם על ידי %s</string>
<string name="la_no_app_password">אין קוד גישה לאפליקציה</string>
<string name="videos_limit_desc">ניתן לשלוח רק 10 סרטונים בו־זמנית</string>
@@ -674,7 +672,6 @@
<string name="network_use_onion_hosts_required_desc">יידרשו מארחי onion לחיבור.
\nשימו לב: לא תוכלו להתחבר לשרתים ללא כתובת .onion.</string>
<string name="network_use_onion_hosts_no_desc">לא ייעשה שימוש במארחי Onion.</string>
<string name="network_use_onion_hosts_no_desc_in_alert">לא ייעשה שימוש במארחי Onion.</string>
<string name="callstatus_missed">שיחה שלא נענתה</string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[רק מכשירי לקוח מאחסנים פרופילי משתמש, אנשי קשר, קבוצות, והודעות שנשלחו עם <b>הצפנה מקצה־לקצה דו־שכבתית</b>.]]></string>
<string name="status_no_e2e_encryption">ללא הצפנה מקצה־לקצה</string>
@@ -902,7 +899,6 @@
<string name="custom_time_unit_seconds">שניות</string>
<string name="custom_time_picker_select">אישור</string>
<string name="role_in_group">תפקיד</string>
<string name="network_options_revert">ביטול</string>
<string name="save_and_update_group_profile">שמור ועדכן את פרופיל הקבוצה</string>
<string name="save_welcome_message_question">לשמור הודעת פתיחה\?</string>
<string name="current_version_timestamp">%s (נוכחי)</string>
@@ -1077,7 +1073,6 @@
<string name="to_share_with_your_contact">(כדי לשתף עם איש הקשר שלך)</string>
<string name="to_start_a_new_chat_help_header">כדי להתחיל צ׳אט חדש</string>
<string name="smp_servers_use_server_for_new_conn">השתמש עבור חיבורים חדשים</string>
<string name="update_onion_hosts_settings_question">לעדכן הגדרות מארחי ‪.onion‬\?‬</string>
<string name="to_verify_compare">כדי לאמת הצפנה מקצה־לקצה עם איש הקשר שלכם, יש להשוות (או לסרוק) את הקוד במכשירים שלכם.</string>
<string name="your_chat_profiles">פרופילי צ׳אט</string>
<string name="update_network_session_mode_question">לעדכן מצב בידוד תעבורה\?</string>
@@ -1815,7 +1810,7 @@
<string name="saved_chat_item_info_tab">נשמר</string>
<string name="saved_from_chat_item_info_title">נשמר מ</string>
<string name="recipients_can_not_see_who_message_from">מקבלי ההודעה לא יוכלו לראות מי שלח את ההודעה</string>
<string name="forward_chat_item">הועבר</string>
<string name="forward_chat_item">העבר</string>
<string name="forward_message">מעביר הודעה…</string>
<string name="simplex_links_not_allowed">קישורי SimpleX לא מאופשרים</string>
<string name="network_smp_proxy_mode_private_routing">מסלול פרטי</string>
@@ -1840,4 +1835,96 @@
<string name="chat_theme_apply_to_light_mode">ערכת נושא בהירה</string>
<string name="color_mode_system">מערכת</string>
<string name="error_showing_desktop_notification">שגיאה בהצגת התראה, צור קשר עם המפתחים</string>
<string name="smp_proxy_error_connecting">שגיאה בהתחברות לשרת %1$s, אנא נסה מאוחר יותר</string>
<string name="message_forwarded_desc">אין עדיין חיבור ישיר, ההודעה תעובר ע\"י מנהל.</string>
<string name="member_inactive_title">חבר לא פעיל</string>
<string name="xftp_servers_other">שרתי XFTP אחרים</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_installed_successfully_title">הותקן בהצלחה</string>
<string name="app_check_for_updates_button_install">התקן עדכון</string>
<string name="app_check_for_updates_button_open">פתח מיקום קובץ</string>
<string name="app_check_for_updates_installed_successfully_desc">אנא הפעל מחדש את האפילקציה.</string>
<string name="app_check_for_updates_button_remind_later">הזכר מאוחר יותר</string>
<string name="app_check_for_updates_button_skip">דלג על הגרסא הזאת</string>
<string name="app_check_for_updates_notice_disable">השבת</string>
<string name="appearance_font_size">גדול פונט</string>
<string name="current_user">פרופיל נוכחי</string>
<string name="servers_info_messages_sent">הודעות שנשלחו</string>
<string name="expired_label">פג תוקף</string>
<string name="send_errors">שגיאות בשליחה</string>
<string name="size">גודל</string>
<string name="uploaded_files">קבצים שהועלו</string>
<string name="v5_7_quantum_resistant_encryption_descr">יחול בצ\'אטים ישירים!</string>
<string name="audio_device_bluetooth">בלוטוס</string>
<string name="privacy_media_blur_radius">טשטש מדיה</string>
<string name="privacy_media_blur_radius_off">כבוי</string>
<string name="privacy_media_blur_radius_medium">בינוני</string>
<string name="privacy_media_blur_radius_strong">חזק</string>
<string name="member_info_member_disabled">מושבת</string>
<string name="member_info_member_inactive">לא פעיל</string>
<string name="servers_info_details">פרטים</string>
<string name="servers_info_modal_error_title">שגיאה</string>
<string name="servers_info_reconnect_server_error">שגיאה בהתחברות מחדש לשרת</string>
<string name="servers_info_reconnect_servers_error">שגיאה בהתחברות מחדש לשרתים</string>
<string name="servers_info_sessions_errors">שגיאות</string>
<string name="servers_info_subscriptions_section_header">מקבל ההודעות</string>
<string name="servers_info_proxied_servers_section_header">שרתי פרוקסי</string>
<string name="servers_info_reconnect_all_servers_button">חבר מחדש את לכל השרתים</string>
<string name="servers_info_reconnect_server_title">להתחבר מחדש לשרת?</string>
<string name="servers_info_reconnect_servers_title">להתחבר מחדש לשרתים?</string>
<string name="servers_info_statistics_section_header">סטטיסטיקות</string>
<string name="servers_info_subscriptions_total">סך הכל</string>
<string name="attempts_label">ניסיונות</string>
<string name="completed">הושלם</string>
<string name="connections">חיבורים</string>
<string name="deleted">נמחק</string>
<string name="deletion_errors">שגיאות במחיקה</string>
<string name="servers_info_reset_stats_alert_error_title">שגיאה באיפוס הסטטיסטיקה</string>
<string name="other_label">אחר</string>
<string name="secured">מאובטח</string>
<string name="sent_directly">שלח ישירות</string>
<string name="sent_via_proxy">נשלח דרך פרוקסי</string>
<string name="downloaded_files">קבצים שהורדו</string>
<string name="download_errors">שגיאות בהורדה</string>
<string name="open_server_settings_button">פתח הגדרות שרת</string>
<string name="server_address">כתובת שרת</string>
<string name="snd_error_quota">חריגה מהקיבולת - הנמען לא קיבל הודעות שנשלחו בעבר.</string>
<string name="app_check_for_updates">בדוק עבור עדכונים</string>
<string name="servers_info_sessions_connected">מחובר</string>
<string name="servers_info_connected_servers_section_header">שרתים מחוברים</string>
<string name="app_check_for_updates_notice_title">בודק עבור עדכונים חדשים</string>
<string name="servers_info_sessions_connecting">מתחבר</string>
<string name="created">נוצר</string>
<string name="message_forwarded_title">הודעה הועברה</string>
<string name="member_inactive_desc">ההודעה תוכל להימסר מאוחר יותר אם החבר יהפוך לפעיל.</string>
<string name="servers_info_messages_received">הודעות שהתקבלו</string>
<string name="servers_info_reset_stats">אפס את כל הסטטיסטיקות</string>
<string name="wallpaper_scale">גודל</string>
<string name="simplex_links_are_prohibited_in_group">קישורי SimpleX לא מאופשרים בקבוצה הזו.</string>
<string name="scan_paste_link">סרוק/ הדבק קישור</string>
<string name="please_try_later">אנא נסה מאוחר יותר</string>
<string name="smp_servers_other">שרתי SMP אחרים</string>
<string name="servers_info_reset_stats_alert_confirm">אפס</string>
<string name="servers_info_uploaded">הועלה</string>
<string name="servers_info_detailed_statistics">סטטיסטיקה מפורטת</string>
<string name="app_check_for_updates_beta">בטא</string>
<string name="servers_info_reset_stats_alert_title">לאפס את כל הסטטיסטיקות?</string>
<string name="decryption_errors">שגיאות בפענוח</string>
<string name="other_errors">שגיאות אחרות</string>
<string name="upload_errors">שגיאה בהעלאה</string>
<string name="app_check_for_updates_download_completed_title">יורד עדכון לאפליקציה</string>
<string name="app_check_for_updates_download_started">מוריד עדכון לאפליקציה, אל תסגור את האפליקציה</string>
<string name="all_users">כל הפרופילים</string>
<string name="servers_info_files_tab">קבצים</string>
<string name="servers_info_missing">אין מידע, נסה לרענן</string>
<string name="servers_info">מידע על השרתים</string>
<string name="servers_info_detailed_statistics_received_total">התקבל סה\"כ</string>
<string name="servers_info_detailed_statistics_receive_errors">התקבלו שגיאות</string>
<string name="reconnect">התחבר מחדש</string>
<string name="servers_info_detailed_statistics_sent_messages_header">שלח הודעות</string>
<string name="servers_info_detailed_statistics_sent_messages_total">נשלח בסה\"כ</string>
<string name="smp_server">שרת SMP</string>
<string name="xftp_server">שרת XFTP</string>
</resources>
@@ -204,7 +204,6 @@
<string name="display_name_connection_established">接続済み</string>
<string name="connect_via_link">リンク経由で繋がる。</string>
<string name="connection_error">接続エラー</string>
<string name="network_use_onion_hosts_required_desc_in_alert">接続にオニオンのホストが必要となります。</string>
<string name="group_member_status_introduced">接続待ち (紹介済み)</string>
<string name="connection_error_auth">接続エラー (AUTH)</string>
<string name="connection_timeout">接続タイムアウト</string>
@@ -222,7 +221,6 @@
<string name="icon_descr_context">追加情報アイコン</string>
<string name="network_use_onion_hosts_prefer_desc">オニオンのホストが利用可能時に使われます。</string>
<string name="network_use_onion_hosts_no_desc">オニオンのホストが使われません。</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">オニオンのホストが利用可能時に使われます。</string>
<string name="images_limit_desc">画像を1回で最大10枚を送信できます。</string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[<b>2層エンドツーエンド暗号化</b>で送信されたプロフィール、連絡先、グループ、メッセージは、クライント端末にしか保存されません。]]></string>
<string name="only_group_owners_can_change_prefs">グループ設定を変えられるのはグループのオーナーだけです。</string>
@@ -315,7 +313,6 @@
<string name="enter_one_ICE_server_per_line">ICEサーバ (1行に1サーバ)</string>
<string name="network_and_servers">ネットワークとサーバ</string>
<string name="network_settings_title">ネットワーク設定</string>
<string name="network_use_onion_hosts_no_desc_in_alert">オニオンのホストが使われません。</string>
<string name="delete_address">アドレスを削除</string>
<string name="exit_without_saving">保存せずに閉じる</string>
<string name="display_name_cannot_contain_whitespace">表示の名前には空白が使用できません。</string>
@@ -735,7 +732,6 @@
<string name="your_ICE_servers">あなたのICEサーバ</string>
<string name="network_disable_socks">直接にインタネットに繋がりますか?</string>
<string name="network_enable_socks">SOCKSプロキシを使いますか?</string>
<string name="update_onion_hosts_settings_question">.onionのホスト設定を更新しますか?</string>
<string name="network_use_onion_hosts">.onionホストを使う</string>
<string name="network_use_onion_hosts_prefer">利用可能時に</string>
<string name="network_session_mode_transport_isolation">トランスポート隔離</string>
@@ -883,7 +879,6 @@
<string name="you_have_to_enter_passphrase_every_time">アプリ起動時にパスフレーズを入力しなければなりません。端末に保存されてません。</string>
<string name="database_backup_can_be_restored">データベースのパスフレーズ変更が完了してません。</string>
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">リンク、またはQRコードを共有できます。誰でもグループに参加できます。後で削除しても、グループのメンバーがそのままのこります。</string>
<string name="network_options_revert">元に戻す</string>
<string name="update_network_settings_confirmation">更新</string>
<string name="accept_feature_set_1_day">1日に設定</string>
<string name="v4_4_disappearing_messages_desc">一定時間が経ったら送信されたメッセージが削除されます。</string>
@@ -635,7 +635,6 @@
<string name="messages_section_description">이 설정은 현재 내 프로필의 메시지에 적용되어요.</string>
<string name="member_info_section_title_member">멤버</string>
<string name="member_role_will_be_changed_with_invitation">역할이 \"%s\"(으)로 변경되고, 회원은 새로운 초대를 받게 될 거예요.</string>
<string name="network_options_revert">되돌리기</string>
<string name="message_deletion_prohibited">이 채팅에서는 메시지 영구 삭제가 허용되지 않았어요.</string>
<string name="leave_group_button">나가기</string>
<string name="large_file">큰 파일!</string>
@@ -667,9 +666,6 @@
<string name="network_use_onion_hosts_prefer_desc">사용 가능한 경우 Onion 호스트가 사용될 거예요.</string>
<string name="network_use_onion_hosts_no_desc">Onion 호스트가 사용되지 않을 거예요.</string>
<string name="network_session_mode_transport_isolation">전송 격리</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion 호스트가 사용되지 않을 거예요.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">사용 가능한 경우 Onion 호스트가 사용될 거예요.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">연결하려면 Onion 호스트가 필요해요.</string>
<string name="next_generation_of_private_messaging">차세대 사생활 보호 메시징</string>
<string name="new_passphrase">새 비밀번호…</string>
<string name="network_option_enable_tcp_keep_alive">TCP 연결 유지 활성화</string>
@@ -130,7 +130,6 @@
<string name="snd_group_event_group_profile_updated">grupės profilis atnaujintas</string>
<string name="info_row_group">Grupė</string>
<string name="users_delete_question">Ištrinti pokalbio profilį\?</string>
<string name="network_options_revert">Sugrąžinti</string>
<string name="network_options_save">Įrašyti</string>
<string name="feature_enabled">įjungta</string>
<string name="delete_after">Ištrinti po</string>
@@ -1373,7 +1372,6 @@
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(nuskanuokite ar įklijuokite iš iškarpinės)</string>
<string name="you_accepted_connection">Priėmėte prisijungimą</string>
<string name="you_invited_a_contact">Jūs pakvietėte kontaktą</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Onion serveriai bus reikalingi ryšiui.</string>
<string name="network_use_onion_hosts_prefer_desc">Onion serveriai bus naudojami, kai tik bus.</string>
<string name="exit_without_saving">Išeiti neišsaugant</string>
<string name="you_control_your_chat">Jūs kontroliuojate savo pokalbį!</string>
@@ -1565,7 +1563,6 @@
<string name="your_simplex_contact_address">Jūsų SimpleX adresas</string>
<string name="smp_servers_scan_qr">Nuskanuoti serverio QR kodą</string>
<string name="network_use_onion_hosts_required">Reikalingi</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion serveriai bus naudojami, kai tik bus.</string>
<string name="network_use_onion_hosts_no_desc">Onion serveriai nebus naudojami.</string>
<string name="self_destruct">Savaiminis susinaikinimas</string>
<string name="old_database_archive">Senas duomenų bazės archyvas</string>
@@ -1628,8 +1625,6 @@
<string name="moderate_verb">Moderuoti</string>
<string name="contact_wants_to_connect_with_you">nori prisijungti prie jūsų!</string>
<string name="image_descr_link_preview">nuorodos peržiūros nuotrauka</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion serveriai nebus naudojami.</string>
<string name="update_onion_hosts_settings_question">Atnaujinti .onion serverių nustatymą?</string>
<string name="onboarding_notifications_mode_off">Kai programėlė yra paleista</string>
<string name="lock_mode">Užrakto režimas</string>
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Galite paleisti pokalbius per programėlės nustatymus/ duomenų bazę arba paleisdami programėlę iš naujo.</string>
@@ -320,7 +320,6 @@
<string name="switch_receiving_address">സ്വീകരിക്കുന്ന വിലാസം മാറുക</string>
<string name="conn_stats_section_title_servers">സെർവറുകൾ</string>
<string name="network_options_save">സംരക്ഷിക്കുക</string>
<string name="network_options_revert">പഴയപടിയാക്കുക</string>
<string name="theme_system">സംവിധാനം</string>
<string name="language_system">സംവിധാനം</string>
<string name="color_title">ശീർഷകം</string>
@@ -546,7 +546,6 @@
<string name="one_time_link">Eenmalige uitnodiging link</string>
<string name="paste_button">Plakken</string>
<string name="smp_servers_preset_address">Vooraf ingesteld server adres</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion hosts worden niet gebruikt.</string>
<string name="onboarding_notifications_mode_title">Privé meldingen</string>
<string name="paste_the_link_you_received">Plak de link die je hebt ontvangen</string>
<string name="onboarding_notifications_mode_periodic">Periodiek</string>
@@ -573,7 +572,6 @@
<string name="enter_passphrase_notification_title">Wachtwoord is nodig</string>
<string name="feature_off">uit</string>
<string name="add_contact">Eenmalige uitnodiging link</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Onion hosts zijn vereist voor verbinding.</string>
<string name="only_group_owners_can_change_prefs">Alleen groep eigenaren kunnen groep voorkeuren wijzigen.</string>
<string name="only_stored_on_members_devices">(alleen opgeslagen door groepsleden)</string>
<string name="smp_servers_preset_server">Vooraf ingestelde server</string>
@@ -584,7 +582,6 @@
<string name="ok">OK</string>
<string name="network_use_onion_hosts_required_desc">Onion hosts zijn vereist voor verbinding.</string>
<string name="network_use_onion_hosts_prefer_desc">Onion hosts worden gebruikt indien beschikbaar.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion hosts worden gebruikt indien beschikbaar.</string>
<string name="network_use_onion_hosts_no_desc">Onion hosts worden niet gebruikt.</string>
<string name="opensource_protocol_and_code_anybody_can_run_servers">Open-source protocol en code. Iedereen kan de servers draaien.</string>
<string name="people_can_connect_only_via_links_you_share">Mensen kunnen alleen verbinding met u maken via de links die u deelt.</string>
@@ -808,7 +805,6 @@
<string name="network_options_reset_to_defaults">Resetten naar standaardwaarden</string>
<string name="switch_receiving_address">Ontvangst adres wijzigen</string>
<string name="network_option_protocol_timeout">Protocol timeout</string>
<string name="network_options_revert">Terugdraaien</string>
<string name="network_options_save">Opslaan</string>
<string name="network_option_seconds_label">sec</string>
<string name="incognito_info_share">Wanneer je een incognito profiel met iemand deelt, wordt dit profiel gebruikt voor de groepen waarvoor ze je uitnodigen.</string>
@@ -904,7 +900,6 @@
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[U kunt <font color="#0088ff">verbinding maken met SimpleX Chat ontwikkelaars om vragen te stellen en updates te ontvangen</font>.]]></string>
<string name="connection_error_auth_desc">Tenzij uw contact de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft.
\nOm verbinding te maken, vraagt u uw contact om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft.</string>
<string name="update_onion_hosts_settings_question">.onion hosts-instelling updaten\?</string>
<string name="use_simplex_chat_servers__question">SimpleX Chat servers gebruiken\?</string>
<string name="voice_messages_are_prohibited">Spraak berichten zijn verboden in deze groep.</string>
<string name="personal_welcome">Welkom %1$s!</string>
@@ -1987,4 +1982,55 @@
<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>
<string name="privacy_media_blur_radius">Vervaag media</string>
<string name="privacy_media_blur_radius_medium">Medium</string>
<string name="privacy_media_blur_radius_off">uit</string>
<string name="privacy_media_blur_radius_soft">Soft</string>
<string name="privacy_media_blur_radius_strong">Krachtig</string>
<string name="proxy_destination_error_broker_host">Het doelserveradres van %1$s is niet compatibel met de instellingen van de doorstuurserver %2$s.</string>
<string name="proxy_destination_error_broker_version">De doelserverversie van %1$s is incompatibel met de doorstuurserver %2$s.</string>
<string name="smp_proxy_error_connecting">Fout bij verbinden met doorstuurserver %1$s. Probeer het later opnieuw.</string>
<string name="proxy_destination_error_failed_to_connect">Doorstuurserver %1$s kon geen verbinding maken met bestemmingsserver %2$s. Probeer het later opnieuw.</string>
<string name="smp_proxy_error_broker_host">Het doorstuuradres is niet compatibel met de netwerkinstellingen: %1$s.</string>
<string name="smp_proxy_error_broker_version">De doorstuurserverversie is niet compatibel met de netwerkinstellingen: %1$s.</string>
<string name="cant_call_contact_alert_title">Kan contact niet bellen</string>
<string name="allow_calls_question">Oproepen toestaan?</string>
<string name="info_view_call_button">bellen</string>
<string name="cant_call_member_alert_title">Kan geen groepslid bellen</string>
<string name="calls_prohibited_alert_title">Bellen verboden!</string>
<string name="confirm_delete_contact_question">Contact verwijderen bevestigen?</string>
<string name="conversation_deleted">Gesprek verwijderd!</string>
<string name="delete_without_notification">Verwijderen zonder melding</string>
<string name="deleted_chats">Verwijderde chats</string>
<string name="cant_call_contact_deleted_alert_text">Contact is verwijderd.</string>
<string name="cant_call_contact_connecting_wait_alert_text">Er wordt verbinding gemaakt met het contact. Even geduld of controleer het later!</string>
<string name="cant_send_message_to_member_alert_title">Kan geen bericht sturen naar groepslid</string>
<string name="delete_members_messages__question">%d berichten van leden verwijderen?</string>
<string name="compose_message_placeholder">Bericht</string>
<string name="delete_messages_mark_deleted_warning">Berichten worden gemarkeerd voor verwijdering. De ontvanger(s) kunnen deze berichten onthullen.</string>
<string name="selected_chat_items_nothing_selected">Niets geselecteerd</string>
<string name="info_view_connect_button">verbinden</string>
<string name="contact_deleted">Contact verwijderd!</string>
<string name="delete_contact_cannot_undo_warning">Het contact wordt verwijderd. Dit kan niet ongedaan worden gemaakt!</string>
<string name="keep_conversation">Blijf in gesprek</string>
<string name="info_view_message_button">bericht</string>
<string name="only_delete_conversation">Alleen conversatie verwijderen</string>
<string name="info_view_open_button">open</string>
<string name="paste_link">Plak de link</string>
<string name="action_button_add_members">Uitnodiging</string>
<string name="one_hand_ui">Gebruikersinterface met één hand</string>
<string name="calls_prohibited_ask_to_enable_calls_alert_text">Vraag uw contactpersoon om oproepen in te schakelen.</string>
<string name="no_filtered_contacts">Geen gefilterde contacten</string>
<string name="select_verb">Selecteer</string>
<string name="selected_chat_items_selected_n">Geselecteerd %d</string>
<string name="cant_call_member_send_message_alert_text">Stuur een bericht om oproepen mogelijk te maken.</string>
<string name="info_view_search_button">zoekopdracht</string>
<string name="you_can_still_send_messages_to_contact">Je kunt nog steeds berichten sturen naar %1$s vanuit de Verwijderde chats.</string>
<string name="toolbar_settings">Instellingen</string>
<string name="moderate_messages_will_be_deleted_warning">De berichten worden voor alle leden verwijderd.</string>
<string name="moderate_messages_will_be_marked_warning">De berichten worden voor alle leden als gemodereerd gemarkeerd.</string>
<string name="info_view_video_button">video</string>
<string name="you_can_still_view_conversation_with_contact">Je kunt nog steeds het gesprek met %1$s bekijken in de lijst met chats.</string>
<string name="you_need_to_allow_calls">U moet uw contactpersoon toestemming geven om te bellen, zodat hij/zij je kan bellen.</string>
<string name="contact_list_header_title">Jouw contacten</string>
</resources>
@@ -348,14 +348,11 @@
<string name="network_use_onion_hosts_no">Nie</string>
<string name="network_use_onion_hosts_required_desc">Hosty onion będą wymagane do połączenia.
\nUwaga: nie będziesz mógł połączyć się z serwerami bez adresu .onion.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Hosty onion będą wymagane do połączenia.</string>
<string name="network_use_onion_hosts_prefer_desc">Hosty onion będą używane, gdy będą dostępne.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Hosty onion będą używane, gdy będą dostępne.</string>
<string name="network_use_onion_hosts_no_desc">Hosty onion nie będą używane.</string>
<string name="network_use_onion_hosts_required">Wymagane</string>
<string name="save_servers_button">Zapisz</string>
<string name="network_session_mode_transport_isolation">Izolacja transportu</string>
<string name="update_onion_hosts_settings_question">Zaktualizować ustawienie hostów .onion\?</string>
<string name="update_network_session_mode_question">Zaktualizować tryb izolacji transportu\?</string>
<string name="network_disable_socks">Użyć bezpośredniego połączenia z Internetem\?</string>
<string name="network_use_onion_hosts">Użyj hostów .onion</string>
@@ -758,7 +755,6 @@
<string name="users_delete_with_connections">Profil i połączenia z serwerem</string>
<string name="network_option_protocol_timeout">Limit czasu protokołu</string>
<string name="network_options_reset_to_defaults">Przywróć wartości domyślne</string>
<string name="network_options_revert">Przywrócić</string>
<string name="network_options_save">Zapisz</string>
<string name="network_option_seconds_label">sek</string>
<string name="tap_to_activate_profile">Dotknij, aby aktywować profil.</string>
@@ -953,7 +949,6 @@
<string name="gallery_video_button">Wideo</string>
<string name="feature_offered_item_with_param">zaproponował %s: %2s</string>
<string name="only_group_owners_can_enable_voice">Tylko właściciele grup mogą włączyć wiadomości głosowe.</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Hosty onion nie będą używane.</string>
<string name="only_your_contact_can_delete">Tylko Twój kontakt może nieodwracalnie usunąć wiadomości (możesz oznaczyć je do usunięcia). (24 godziny)</string>
<string name="restore_passphrase_not_found_desc">Hasło nie zostało znalezione w Keystore, wprowadź je ręcznie. Może się tak zdarzyć, gdy przywrócisz dane aplikacji za pomocą narzędzia do kopii zapasowych. Jeśli tak nie jest, skontaktuj się z programistami.</string>
<string name="group_members_can_send_disappearing">Członkowie grupy mogą wysyłać znikające wiadomości.</string>
@@ -482,7 +482,6 @@
<string name="old_database_archive">Arquivo de banco de dados antigo</string>
<string name="button_add_members">Convidar membros</string>
<string name="no_contacts_selected">Nenhum contato selecionado</string>
<string name="network_options_revert">Reverter</string>
<string name="network_options_save">Salvar</string>
<string name="reset_color">Redefinir cores</string>
<string name="v4_5_italian_interface">interface italiana</string>
@@ -647,7 +646,6 @@
<string name="network_use_onion_hosts_no_desc">Onion hosts não serão usados.</string>
<string name="network_use_onion_hosts_required_desc">Os hosts Onion serão necessários para a conexão.
\nAtenção: você não será capaz de se conectar aos servidores sem um endereço .onion</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Os hosts Onion serão necessários para a conexão.</string>
<string name="core_version">Versão principal: v%s</string>
<string name="read_more_in_github_with_link"><![CDATA[Leia mais no nosso <font color="#0088ff">repositório do GitHub</font>.]]></string>
<string name="onboarding_notifications_mode_subtitle">Pode ser mudado mais tarde via configurações.</string>
@@ -761,9 +759,7 @@
<string name="v4_6_hidden_chat_profiles_descr">Proteja seus perfis de bate-papo com uma senha!</string>
<string name="this_text_is_available_in_settings">Este texto está disponível nas configurações</string>
<string name="scan_code">Escanear código</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Hosts Onion não serão usados.</string>
<string name="network_use_onion_hosts_prefer_desc">Os hosts Onion serão usados quando disponíveis.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Os hosts Onion serão usados quando disponíveis.</string>
<string name="your_current_profile">Seu perfil atual</string>
<string name="privacy_redefined">Privacidade redefinida</string>
<string name="onboarding_notifications_mode_title">Notificações privadas</string>
@@ -963,7 +959,6 @@
<string name="share_message">Compartilhar mensagem…</string>
<string name="personal_welcome">Bem-vindo(a) %1$s!</string>
<string name="group_preview_you_are_invited">você está convidado para o grupo</string>
<string name="update_onion_hosts_settings_question">Atualizar configuração de hosts .onion\?</string>
<string name="use_chat">Usar bate-papo</string>
<string name="voice_prohibited_in_this_chat">Mensagens de voz são proibidas neste chat.</string>
<string name="video_descr">Vídeo</string>
@@ -39,7 +39,7 @@
<string name="users_delete_data_only">Apenas dados de perfil local</string>
<string name="color_primary">Realçar</string>
<string name="chat_preferences_you_allow">Você permite</string>
<string name="timed_messages">Mensagens que desaparecem</string>
<string name="timed_messages">Mensagens temporárias</string>
<string name="chat_preferences_always">sempre</string>
<string name="chat_preferences_no">não</string>
<string name="set_group_preferences">Definir preferências de grupo</string>
@@ -62,12 +62,12 @@
<string name="ttl_min">%d min</string>
<string name="ttl_month">%d mês</string>
<string name="ttl_months">%d meses</string>
<string name="ttl_mth">%dmês</string>
<string name="ttl_mth">%dº mês</string>
<string name="v4_2_group_links_desc">Administradores podem criar as ligações para entrar em grupos.</string>
<string name="v4_3_voice_messages">Mensagens de voz</string>
<string name="v4_2_auto_accept_contact_requests">Aceitar automaticamente pedidos de contato</string>
<string name="v4_3_improved_server_configuration_desc">Adicionar servidores lendo QR codes.</string>
<string name="v4_4_disappearing_messages">Mensagens que desaparecem</string>
<string name="v4_4_disappearing_messages">Mensagens temporárias</string>
<string name="v4_4_live_messages">Mensagens ao vivo</string>
<string name="v4_4_disappearing_messages_desc">As mensagens enviadas serão eliminadas após o tempo definido.</string>
<string name="v4_5_message_draft">Mensagem de rascunho</string>
@@ -138,17 +138,17 @@
<string name="chat_archive_header">Arquivo de conversa</string>
<string name="delete_group_menu_action">Eliminar</string>
<string name="delete_files_and_media_all">Eliminar todos os ficheiros</string>
<string name="delete_archive">Eliminar ficheiro</string>
<string name="delete_archive">Eliminar arquivo</string>
<string name="delete_chat_archive_question">Eliminar arquivo de conversa\?</string>
<string name="delete_database">Eliminar base de dados</string>
<string name="chat_database_section">BASE DE DADOS DE CONVERSA</string>
<string name="chat_database_deleted">Base de dados de conversa eliminada</string>
<string name="display_name">Nome para Exibição</string>
<string name="show_dev_options">Mostrar:</string>
<string name="deleted_description">eliminada</string>
<string name="deleted_description">eliminado</string>
<string name="rcv_group_event_group_deleted">grupo eliminado</string>
<string name="group_display_name_field">Nome do grupo:</string>
<string name="display_name_cannot_contain_whitespace">O nome para exibição não pode conter espaços em branco.</string>
<string name="display_name_cannot_contain_whitespace">O nome de exibição não pode conter espaços em branco.</string>
<string name="ttl_m">%dm</string>
<string name="dont_show_again">Não mostrar novamente</string>
<string name="show_developer_options">Mostrar opções de desenvolvedor</string>
@@ -157,9 +157,9 @@
<string name="allow_to_send_disappearing">Permitir enviar mensagens que desaparecem.</string>
<string name="display_name__field">Nome para Exibição:</string>
<string name="show_QR_code">Mostrar código QR</string>
<string name="disappearing_messages_are_prohibited">Mensagens que desaparecem são proibidas neste grupo.</string>
<string name="connect_via_link_or_qr">Conectar via ligação / código QR</string>
<string name="disappearing_prohibited_in_this_chat">Mensagens que desaparecem são proibidas nesta conversa.</string>
<string name="disappearing_messages_are_prohibited">Mensagens temporárias são proibidas neste grupo.</string>
<string name="connect_via_link_or_qr">Conectar via link / código QR</string>
<string name="disappearing_prohibited_in_this_chat">Mensagens temporárias são proibidas nesta conversa.</string>
<string name="send_verb">Enviar</string>
<string name="live">AO VIVO</string>
<string name="send_live_message_desc">Enviar uma mensagem ao vivo - ela será atualizada para o(s) destinatário(s) à medida que você a digita</string>
@@ -251,7 +251,7 @@
<string name="chat_database_imported">Base de dados de conversa importada</string>
<string name="chat_is_stopped">A conversa está parada</string>
<string name="smp_servers_check_address">Verifique o endereço do servidor e tente novamente.</string>
<string name="profile_will_be_sent_to_contact_sending_link">O seu perfil será enviado para o contato do qual você recebeu esta ligação.</string>
<string name="profile_will_be_sent_to_contact_sending_link">O seu perfil será enviado para o contacto do qual você recebeu esta ligação.</string>
<string name="error_deleting_database">Erro ao eliminar a base de dados de conversa</string>
<string name="error_deleting_link_for_group">Erro ao eliminar ligação de grupo</string>
<string name="network_session_mode_user">Perfil de conversa</string>
@@ -270,14 +270,14 @@
<string name="change_role">Alterar função</string>
<string name="change_member_role_question">Alterar a função no grupo\?</string>
<string name="error_changing_role">Erro ao alterar função</string>
<string name="chat_preferences_contact_allows">O contato permite</string>
<string name="chat_preferences_contact_allows">O contacto permite</string>
<string name="chat_preferences">Preferências de conversa</string>
<string name="app_name">SimpleX</string>
<string name="thousand_abbreviation">m</string>
<string name="connect_via_contact_link">Conectar através da ligação de contato\?</string>
<string name="connect_via_invitation_link">Conectar via convite de ligação\?</string>
<string name="connect_via_contact_link">Conectar através do endereço de contacto?</string>
<string name="connect_via_invitation_link">Conectar via link de convite?</string>
<string name="connect_via_group_link">Conectar através da ligação do grupo\?</string>
<string name="you_will_join_group">Você irá juntar-se a um grupo ao qual esta ligação se refere e conectar-se aos membros do grupo.</string>
<string name="you_will_join_group">Você irá conectar-se a todos os membros do grupo.</string>
<string name="server_error">erro</string>
<string name="failed_to_create_user_title">Erro ao criar perfil!</string>
<string name="error_adding_members">Erro ao adicionar membro(s)</string>
@@ -334,7 +334,7 @@
<string name="group_member_status_accepted">conectando (aceite)</string>
<string name="group_member_status_announced">conectando (anunciado)</string>
<string name="group_member_status_connecting">conectando</string>
<string name="icon_descr_contact_checked">Contato verificado</string>
<string name="icon_descr_contact_checked">Contacto verificado</string>
<string name="group_link">Ligação de grupo</string>
<string name="display_name_connecting">conectando…</string>
<string name="display_name_connection_established">conexão estabelecida</string>
@@ -348,10 +348,10 @@
<string name="icon_descr_server_status_connected">Conectado</string>
<string name="contact_connection_pending">conectando…</string>
<string name="connection_timeout">Tempo limite de conexão</string>
<string name="contact_preferences">Preferências de contato</string>
<string name="notification_preview_somebody">Contato escondido:</string>
<string name="alert_title_contact_connection_pending">O contato ainda não está conectado!</string>
<string name="notification_preview_mode_contact">Nome do contato</string>
<string name="contact_preferences">Preferências de contacto</string>
<string name="notification_preview_somebody">contacto escondido:</string>
<string name="alert_title_contact_connection_pending">O contacto ainda não está conectado!</string>
<string name="notification_preview_mode_contact">Nome do contacto</string>
<string name="contribute">Contribuir</string>
<string name="copy_verb">Copiar</string>
<string name="core_version">Versão principal: v%s</string>
@@ -362,16 +362,16 @@
<string name="connect_via_link">Conectar via ligação</string>
<string name="connection_error">Erro de conexão</string>
<string name="connection_local_display_name">conexão %1$d</string>
<string name="contact_already_exists">O contato já existe</string>
<string name="contact_already_exists">O contacto já existe</string>
<string name="one_time_link">Convite de ligação de utilização única</string>
<string name="save_servers_button">Salvar</string>
<string name="settings_section_title_incognito">Modo anónimo</string>
<string name="rcv_group_event_invited_via_your_group_link">convidado através da ligação do seu grupo</string>
<string name="invite_prohibited_description">Você está a tentar convidar um contato com quem partilhou um perfil anónimo para o grupo no qual voçê está a usar o seu perfil principal</string>
<string name="invite_prohibited_description">Você está a tentar convidar um contacto com quem partilhou um perfil anónimo para um grupo no qual você está a usar o seu perfil principal</string>
<string name="info_row_connection">Conexão</string>
<string name="incognito_info_protects">O modo anónimo protege a privacidade do nome e da imagem do seu perfil principal — para cada novo contato um novo perfil aleatório é criado.</string>
<string name="description_you_shared_one_time_link">você partilhou ligação de utilização única</string>
<string name="description_you_shared_one_time_link_incognito">você partilhou ligação anónima de utilização única</string>
<string name="description_you_shared_one_time_link">você partilhou a ligação de utilização única</string>
<string name="description_you_shared_one_time_link_incognito">você partilhou a ligação anónima de utilização única</string>
<string name="invalid_connection_link">Ligação de conexão inválida</string>
<string name="save_verb">Salvar</string>
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Se você recebeu convite de ligação do SimpleX Chat, você pode abri-lo no seu navegador:</string>
@@ -397,9 +397,9 @@
<string name="simplex_link_mode_description">Descrição</string>
<string name="simplex_link_mode_full">Ligação completa</string>
<string name="description_via_contact_address_link_incognito">anónimo via ligação de endereço de contato</string>
<string name="simplex_link_mode_browser_warning">Abrir a ligação no navegador pode reduzir a privacidade e a segurança da ligação. As ligações Simplex não confiáveis serão vermelhas.</string>
<string name="simplex_link_mode_browser_warning">Abrir a ligação no browser poderá reduzir a privacidade e a segurança da ligação. As ligações Simplex não confiáveis serão vermelhas.</string>
<string name="auth_confirm_credential">Confirmar credenciais</string>
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Contato e todas as mensagens serão eliminadas - esta acção não pode ser revertida!</string>
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">O contacto e todas as mensagens serão eliminadas - esta ação é irreversível!</string>
<string name="image_descr_link_preview">ligação de visualização de imagem</string>
<string name="invalid_contact_link">Ligação inválida!</string>
<string name="read_more_in_user_guide_with_link"><![CDATA[Leia mais em <font color="#0088ff">Guia de Utilizador</font>.]]></string>
@@ -414,12 +414,12 @@
<string name="add_contact">Convite de ligação de utilização única</string>
<string name="all_your_contacts_will_remain_connected_update_sent">Todos os seus contatos permanecerão conectados. A atualização do perfil será enviada aos seus contatos.</string>
<string name="add_address_to_your_profile">Adicione endereço ao seu perfil, para que os seus contatos possam partilhá-lo com outras pessoas. A atualização do perfil será enviada aos seus contatos.</string>
<string name="contacts_can_mark_messages_for_deletion">Os contatos podem marcar mensagens para eliminar; você será capaz de as ver.</string>
<string name="contacts_can_mark_messages_for_deletion">Os contactos podem marcar mensagens para eliminar; você será capaz de as ver.</string>
<string name="share_one_time_link">Criar convite de ligação de utilização única</string>
<string name="description_via_one_time_link_incognito">anónimo via ligação de utilização única</string>
<string name="description_via_group_link_incognito">anónimo via ligação de grupo</string>
<string name="description_via_one_time_link">via ligação de utilização única</string>
<string name="alert_title_cant_invite_contacts_descr">Você está a usar um perfil anónimo para este grupo - para impedir a partilha do seu perfil principal não é permitido convidar contatos</string>
<string name="alert_title_cant_invite_contacts_descr">Você está a usar um perfil anónimo para este grupo - para impedir a partilha do seu perfil principal, não é permitido convidar contactos</string>
<string name="one_time_link_short">Ligação para 1 utilização</string>
<string name="button_create_group_link">Criar ligação</string>
<string name="delete_link_question">Apagar ligação\?</string>
@@ -433,8 +433,8 @@
<string name="share_invitation_link">Partilhar ligação de utilização única</string>
<string name="read_more_in_github_with_link"><![CDATA[Leia mais no nosso repositório <font color="#0088ff">GitHub</font>.]]></string>
<string name="create_profile">Criar perfil</string>
<string name="create_your_profile">Criar perfil</string>
<string name="num_contacts_selected">%d contato(s) selecionado(s)</string>
<string name="create_your_profile">Criar o seu perfil</string>
<string name="num_contacts_selected">%d contacto(s) selecionado(s)</string>
<string name="users_delete_profile_for">Eliminar perfil de conversa para</string>
<string name="full_deletion">Eliminar para todos</string>
<string name="ttl_d">%dd</string>
@@ -447,16 +447,16 @@
<string name="delete_files_and_media_for_all_users">Eliminar ficheiros de todos os perfis de conversa</string>
<string name="delete_messages">Eliminar mensagens</string>
<string name="total_files_count_and_size">%d ficheiros(s) com tamanho total de %s</string>
<string name="current_passphrase">Senha atual…</string>
<string name="delete_pending_connection__question">Eliminar conexão pendente\?</string>
<string name="current_passphrase">Palavra-passe atual…</string>
<string name="delete_pending_connection__question">Eliminar ligação pendente?</string>
<string name="delete_messages_after">Eliminar mensagens após</string>
<string name="users_delete_question">Eliminar perfil de conversa\?</string>
<string name="delete_chat_profile">Eliminar perfil de conversa</string>
<string name="ttl_hour">%d hora</string>
<string name="ttl_hours">%d horas</string>
<string name="database_error">erro de base de dados</string>
<string name="passphrase_is_different">A senha da base de dados é diferente da guardada na Keystore.</string>
<string name="database_passphrase_is_required">A senha da base de dados é necessária para abrir a conversa.</string>
<string name="database_error">Erro de base de dados</string>
<string name="passphrase_is_different">A palavra-passe da base de dados é diferente da armazenada na Keystore.</string>
<string name="database_passphrase_is_required">A palavra-passe da base de dados é necessária para abrir a conversa.</string>
<string name="button_delete_group">Eliminar grupo</string>
<string name="delete_group_question">Eliminar grupo\?</string>
<string name="conn_level_desc_direct">direta</string>
@@ -464,20 +464,20 @@
<string name="delete_profile">Eliminar perfil</string>
<string name="smp_server_test_create_queue">Criar fila</string>
<string name="for_me_only">Eliminar para mim</string>
<string name="auth_disable_simplex_lock">Desabilitar o bloqueio do SimpleX</string>
<string name="auth_disable_simplex_lock">Desativar o bloqueio do SimpleX</string>
<string name="maximum_supported_file_size">Atualmente o tamanho máximo de ficheiro suportado é %1$s.</string>
<string name="button_delete_contact">Eliminar contato</string>
<string name="button_delete_contact">Eliminar contacto</string>
<string name="create_group">Criar grupo secreto</string>
<string name="smp_servers_delete_server">Eliminar servidor</string>
<string name="customize_theme_title">Personalizar tema</string>
<string name="delete_image">Eliminar imagem</string>
<string name="dont_create_address">Não criar endereço</string>
<string name="no_call_on_lock_screen">Desabilitar</string>
<string name="database_passphrase">Senha da base de dados</string>
<string name="no_call_on_lock_screen">Desativar</string>
<string name="database_passphrase">Palavra-passe da base de dados</string>
<string name="create_simplex_address">Criar endereço SimpleX</string>
<string name="info_row_database_id">ID da base de dados</string>
<string name="smp_server_test_delete_file">Eliminar ficheiro</string>
<string name="delete_contact_question">Eliminar contato\?</string>
<string name="delete_contact_question">Eliminar contacto?</string>
<string name="settings_section_title_device">DISPOSITIVO</string>
<string name="direct_messages">Mensagens diretas</string>
<string name="decentralized">Descentralizado</string>
@@ -487,7 +487,7 @@
<string name="ttl_s">%ds</string>
<string name="ttl_sec">%d seg</string>
<string name="ttl_w">%dw</string>
<string name="failed_to_create_user_duplicate_title">Nome para exibição duplicado!</string>
<string name="failed_to_create_user_duplicate_title">Nome de exibição duplicado!</string>
<string name="la_seconds">%d segundos</string>
<string name="file_saved">Ficheiro guardado</string>
<string name="database_upgrade">Atualização da base de dados</string>
@@ -499,7 +499,7 @@
<string name="icon_descr_file">Ficheiro</string>
<string name="file_not_found">Ficheiro não encontrado</string>
<string name="choose_file">Ficheiro</string>
<string name="you_wont_lose_your_contacts_if_delete_address">Você não perderá os seus contatos se eliminar o seu endereço mais tarde.</string>
<string name="you_wont_lose_your_contacts_if_delete_address">Você não irá perder os seus contatos se eliminar o seu endereço mais tarde.</string>
<string name="you_can_hide_or_mute_user_profile">Você pode esconder ou silenciar um perfil de utilizador - pressione-o para o menu.</string>
<string name="icon_descr_video_on">Vídeo ligado</string>
<string name="xftp_servers">Servidores XFTP</string>
@@ -525,7 +525,7 @@
<string name="wrong_passphrase">Senha da base de dados incorreta</string>
<string name="group_member_status_left">esquerda</string>
<string name="wrong_passphrase_title">Senha errada!</string>
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Você deixará de receber mensagens deste grupo. O histórico de mensagens será preservado.</string>
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Você irá deixar de receber mensagens deste grupo. O histórico de mensagens será preservado.</string>
<string name="join_group_question">Juntar-se ao grupo\?</string>
<string name="v4_2_auto_accept_contact_requests_desc">Com mensagem de boas-vindas opcional.</string>
<string name="simplex_link_connection">via %1$s</string>
@@ -558,7 +558,7 @@
<string name="delete_chat_profile_action_cannot_be_undone_warning">Esta ação não pode ser revertida - o seu perfil, contatos, mensagens e ficheiros serão irreversivelmente perdidos.</string>
<string name="enable_automatic_deletion_message">Esta ação não pode ser revertida - as mensagens enviadas e recebidas antes da seleção serão eliminadas. Pode demorar vários minutos.</string>
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">A sua base de dados atual de conversas será ELIMINADA e SUBSTITUÍDA pela importada.
\nEsta ação não pode ser revertida - o seu perfil, contatos, mensagens e ficheiros serão irreversivelmente perdidos.</string>
\nEsta ação é irreversível - o seu perfil, contactos, mensagens e ficheiros serão irreversivelmente perdidos.</string>
<string name="mark_unread">Marcar como não lido</string>
<string name="group_member_role_member">membro</string>
<string name="member_info_section_title_member">MEMBRO</string>
@@ -578,7 +578,6 @@
<string name="settings_notification_preview_title">Pré-visualização de notificação</string>
<string name="message_delivery_error_desc">Muito provavelmente este contato eliminou a conexão consigo.</string>
<string name="this_text_is_available_in_settings">Este texto está disponível nas definições</string>
<string name="update_onion_hosts_settings_question">Atualizar definições de servidores .onion\?</string>
<string name="onboarding_notifications_mode_subtitle">Pode ser alterado mais tarde através das definições.</string>
<string name="settings_section_title_help">AJUDA</string>
<string name="settings_section_title_support">SUPORTE SIMPLEX CHAT</string>
@@ -591,29 +590,28 @@
<string name="new_in_version">Novidades %s</string>
<string name="notification_new_contact_request">Novo pedido de contato</string>
<string name="new_database_archive">Novo arquivo de base de dados</string>
<string name="encrypted_with_random_passphrase">A base de dados está encriptada com uma senha aleatória, você pode alterá-la.</string>
<string name="encrypted_with_random_passphrase">A base de dados está encriptada com uma palavra-passe aleatória, você pode alterá-la.</string>
<string name="enter_correct_passphrase">Insira a senha correta.</string>
<string name="database_backup_can_be_restored">A tentativa de alterar a senha da base de dados não foi concluída.</string>
<string name="database_is_not_encrypted">A sua base de dados de conversas não está encriptada - defina a senha para a proteger.</string>
<string name="database_is_not_encrypted">A sua base de dados de conversas não está encriptada - defina a palavra-passe para a proteger.</string>
<string name="enter_passphrase">Insira a senha…</string>
<string name="you_have_to_enter_passphrase_every_time">Você tem que inserir a senha sempre que a aplicação é iniciada - ela não é guardada no dispositivo.</string>
<string name="you_have_to_enter_passphrase_every_time">Você tem que inserir a palavra-passe sempre que a aplicação é iniciada - ela não é armazenada no dispositivo.</string>
<string name="new_passphrase">Nova senha…</string>
<string name="remove_passphrase">Remover</string>
<string name="remove_passphrase_from_keychain">Remover senha da Keystore\?</string>
<string name="enter_correct_current_passphrase">Insira a senha atual correta.</string>
<string name="update_database_passphrase">Atualizar senha da base de dados</string>
<string name="notification_preview_new_message">nova mensagem</string>
<string name="database_passphrase_and_export">Senha da base de dados &amp; exportação</string>
<string name="set_password_to_export_desc">A base de dados está encriptada com uma senha aleatória. Por favor, altere-a antes de exportar.</string>
<string name="database_passphrase_will_be_updated">A senha de encriptação da base de dados será atualizada.</string>
<string name="database_passphrase_and_export">Palavra-passe da base de dados &amp; exportação</string>
<string name="set_password_to_export_desc">A base de dados está encriptada com uma palavra-passe aleatória. Por favor, altere-a antes de exportar.</string>
<string name="database_passphrase_will_be_updated">A palavra-passe de encriptação da base de dados será atualizada.</string>
<string name="store_passphrase_securely">Por favor armazene a senha de forma segura, você NÃO será capaz de a alterar se a perder.</string>
<string name="database_encryption_will_be_updated">A senha de encriptação da base de dados será atualizada e armazenada na Keystore.</string>
<string name="database_encryption_will_be_updated">A palavra-passe de encriptação da base de dados será atualizada e armazenada nas definições.</string>
<string name="database_will_be_encrypted_and_passphrase_stored">A base de dados será encriptada e a senha armazenada na Keystore.</string>
<string name="store_passphrase_securely_without_recover">Por favor armazene a senha de forma segura, você NÃO será capaz de aceder às conversas se a perder.</string>
<string name="enter_passphrase_notification_desc">Para receber notificações, por favor, digite a senha da base de dados</string>
<string name="network_use_onion_hosts_no_desc">Hosts Onion não serão usados.</string>
<string name="network_use_onion_hosts_prefer_desc">Hosts Onion serão usados quando disponíveis.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Hosts Onion serão necessários para a conexão.</string>
<string name="video_call_no_encryption">chamada de vídeo (sem encriptação ponta a ponta)</string>
<string name="audio_call_no_encryption">chamada de áudio (não encriptada ponta a ponta)</string>
<string name="encrypted_audio_call">chamada de áudio encriptada ponta a ponta</string>
@@ -623,12 +621,11 @@
<string name="feature_offered_item">oferecido %s</string>
<string name="old_database_archive">Arquivo de base de dados antigo</string>
<string name="network_use_onion_hosts_required_desc">Hosts Onion serão necessários para a conexão.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Hosts Onion serão usados quando disponíveis.</string>
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Pode acontecer quando você ou sua conexão usaram o backup de base de dados antigo.</string>
<string name="status_e2e_encrypted">encriptado ponta a ponta</string>
<string name="feature_off">desligado</string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 desktop: leia o código QR exibido a partir do aplicação, via <b>Ler código QR</b>.]]></string>
<string name="status_contact_has_no_e2e_encryption">contato não tem encriptação ponta a ponta</string>
<string name="status_contact_has_no_e2e_encryption">o contacto não tem encriptação ponta a ponta</string>
<string name="feature_offered_item_with_param">oferecido %s: %2s</string>
<string name="chat_preferences_off">desligado</string>
<string name="chat_preferences_on">ligado</string>
@@ -644,9 +641,8 @@
<string name="to_verify_compare">Para verificar a encriptação de ponta a ponta com o seu contato, compare (ou leia) o código nos seus dispositivos.</string>
<string name="scan_code_from_contacts_app">Ler o código de segurança a partir da aplicação do seu contacto.</string>
<string name="smp_servers_scan_qr">Ler o código QR do servidor</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Hosts Onion não serão usados.</string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Apenas dispositivos cliente armazenam perfis de utilizador, contatos, grupos e mensagens enviadas com <b>encriptação de ponta a ponta de 2 camadas</b>.]]></string>
<string name="status_contact_has_e2e_encryption">contato tem encriptação ponta a ponta</string>
<string name="status_contact_has_e2e_encryption">o contacto tem encriptação ponta a ponta</string>
<string name="status_no_e2e_encryption">sem encriptação ponta a ponta</string>
<string name="group_member_status_creator">criador</string>
<string name="custom_time_unit_months">meses</string>
@@ -685,51 +681,51 @@
<string name="icon_descr_email">Email</string>
<string name="network_session_mode_transport_isolation">Isolamento do Transporte</string>
<string name="callstate_ended">terminado</string>
<string name="settings_developer_tools">Ferramentas de desenvolvedor</string>
<string name="settings_developer_tools">Ferramentos de programador</string>
<string name="encrypt_database">Encriptar</string>
<string name="downgrade_and_open_chat">Fazer downgrade e abrir chat</string>
<string name="network_option_enable_tcp_keep_alive">Activar TCP manter-vivo</string>
<string name="network_option_enable_tcp_keep_alive">Ativar TCP keep-alive</string>
<string name="chat_preferences_default">predefinido (%s)</string>
<string name="v4_5_transport_isolation">Isolamento do Transporte</string>
<string name="trying_to_connect_to_server_to_receive_messages">A tentar connectar ao servidor usado para receber mensagens deste contacto.</string>
<string name="image_decoding_exception_title">Erro de descodificação</string>
<string name="developer_options">IDs das bases de dados e opções de isolamento do Transporte</string>
<string name="database_downgrade">Downgrade da base de dados</string>
<string name="auth_enable_simplex_lock">Activar SimpleX Lock</string>
<string name="database_downgrade">Regressão da base de dados</string>
<string name="auth_enable_simplex_lock">Ativar SimpleX Lock</string>
<string name="edit_verb">Editar</string>
<string name="icon_descr_edited">editado</string>
<string name="disappearing_message">Mensagem temporária</string>
<string name="edit_image">Editar imagem</string>
<string name="enable_lock">Activar bloqueio</string>
<string name="enabled_self_destruct_passcode">Activar código de acesso auto-destrutivo</string>
<string name="enable_self_destruct">Activar auto-destruição</string>
<string name="enable_lock">Ativar bloqueio</string>
<string name="enabled_self_destruct_passcode">Ativar código de acesso auto-destrutivo</string>
<string name="enable_self_destruct">Ativar auto-destruição</string>
<string name="empty_chat_profile_is_created">Um perfil vazio é criado com o nome fornecido, e a aplicação abre como de costume.</string>
<string name="mtr_error_no_down_migration">a versão da base de dados é mais recente do que a aplicação, mas sem migração para baixo para: %s</string>
<string name="share_text_disappears_at">Desaparecerá: %s</string>
<string name="mtr_error_no_down_migration">a versão da base de dados é mais recente do que a aplicação, mas sem migração de regressão para: %s</string>
<string name="share_text_disappears_at">Desaparecerá a: %s</string>
<string name="delete_files_and_media_question">Eliminar ficheiros e multimédia\?</string>
<string name="share_text_deleted_at">Eliminado a: %s</string>
<string name="auth_device_authentication_is_disabled_turning_off">A autenticação do dispositivo está desactivada. A desligar SimpleX Lock.</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">A autenticação do dispositivo não está activada. Pode activar o SimpleX Lock através das Definições, depois de activar a autenticação do dispositivo.</string>
<string name="auth_device_authentication_is_disabled_turning_off">A autenticação do dispositivo está desativada. A desligar SimpleX Lock.</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">A autenticação do dispositivo não está ativa. Pode ativar o SimpleX Lock através das Definições, depois de ativar a autenticação do dispositivo.</string>
<string name="mtr_error_different">migração diferente na aplicação/base de dados: %s / %s</string>
<string name="allow_accepting_calls_from_lock_screen">Activar chamadas a partir do ecrã de bloqueio através das Definições.</string>
<string name="allow_accepting_calls_from_lock_screen">Ativar chamadas a partir do ecrã de bloqueio através das Definições.</string>
<string name="encrypt_database_question">Encriptar base de dados\?</string>
<string name="smp_servers_enter_manually">Introduzir o servidor manualmente</string>
<string name="enable_automatic_deletion_question">Activar a eliminação automática de mensagens\?</string>
<string name="enable_automatic_deletion_question">Ativar a eliminação automática de mensagens?</string>
<string name="button_edit_group_profile">Editar perfil de grupo</string>
<string name="decryption_error">Erro de desencriptação</string>
<string name="app_passcode_replaced_with_self_destruct">O código de acesso é substituído por um código auto-destrutivo.</string>
<string name="share_text_database_id">ID da base de dados: %d</string>
<string name="v4_5_multiple_chat_profiles_descr">Nomes, avatares e isolamento de transporte diferentes.</string>
<string name="v4_5_multiple_chat_profiles_descr">Nomes, fotos de perfil e isolamento de transporte diferentes.</string>
<string name="color_secondary_variant">Secundário adicional</string>
<string name="color_primary_variant">Realce adicional</string>
<string name="color_background">Fundo</string>
<string name="v5_1_custom_themes_descr">Customizar e partilhar temas de cor.</string>
<string name="v5_1_custom_themes_descr">Personalizar e partilhar temas de cor.</string>
<string name="v5_1_custom_themes">Temas personalizados</string>
<string name="info_row_deleted_at">Eliminado a</string>
<string name="info_row_disappears_at">Desaparecerá</string>
<string name="feature_enabled">activado</string>
<string name="feature_enabled_for_contact">activado para contacto</string>
<string name="feature_enabled_for_you">activado para si</string>
<string name="info_row_disappears_at">Desaparecerá a</string>
<string name="feature_enabled">Ativo</string>
<string name="feature_enabled_for_contact">ativo para contacto</string>
<string name="feature_enabled_for_you">ativo para si</string>
<string name="search_verb">Pesquisar</string>
<string name="la_mode_off">Desativado</string>
<string name="error_smp_test_failed_at_step">O teste falhou na etapa %s.</string>
@@ -779,4 +775,178 @@
<string name="auth_stop_chat">Parar conversa</string>
<string name="v4_5_italian_interface_descr">Obrigado aos usuários – contribuam via Weblate!</string>
<string name="possible_slow_function_title">Função lenta</string>
<string name="delivery">Entrega</string>
<string name="your_preferences">As suas preferências</string>
<string name="disconnect_remote_hosts">Desconectar dispositivos móveis</string>
<string name="blocked_by_admin_items_description">%d mensagens bloqueadas pelo administrador</string>
<string name="open_port_in_firewall_title">Abrir porta na firewall</string>
<string name="your_privacy">A sua privacidade</string>
<string name="receipts_groups_enable_keep_overrides">Ativar (manter sobreposição de grupo)</string>
<string name="auth_open_migration_to_another_device">Abrir ecrã de migração</string>
<string name="snd_error_relay">Erro de servidor do destino: %1$s</string>
<string name="you_can_turn_on_lock">Você pode ativar o SimpleX Lock através das Definições.</string>
<string name="delete_messages__question">Eliminar %d mensagens?</string>
<string name="create_chat_profile">Criar perfil de chat</string>
<string name="permissions_open_settings">Abrir definições</string>
<string name="receipts_section_contacts">Contactos</string>
<string name="snd_group_event_user_left">você saiu</string>
<string name="snd_group_event_member_deleted">você removeu %1$s</string>
<string name="conn_event_ratchet_sync_ok">encriptação ok</string>
<string name="abort_switch_receiving_address">Cancelar mudança de endereço</string>
<string name="you_will_still_receive_calls_and_ntfs">Você continuará a receber chamadas e notificações de perfis silenciados quando estes estão ativos.</string>
<string name="v5_6_quantum_resistant_encryption_descr">Ativar em conversas diretas (BETA)!</string>
<string name="desktop_address">Endereço de computador</string>
<string name="multicast_discoverable_via_local_network">Descobrível na rede local</string>
<string name="remote_ctrl_error_bad_version">Desktop tem uma versão não suportada. Por favor, confirme que usa a mesma versão em ambos os dispositivos</string>
<string name="remote_ctrl_error_bad_invitation">Desktop tem o código de convite errado</string>
<string name="migrate_to_device_downloading_archive">A transferir arquivo</string>
<string name="migrate_to_device_downloading_details">A transferir detalhes de ligação</string>
<string name="migrate_from_device_creating_archive_link">A criar ligação de arquivo</string>
<string name="duplicates_label">duplicados(as)</string>
<string name="open_server_settings_button">Abrir definições do servidor</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">O seu perfil de conversa será enviado
\npara o seu contacto</string>
<string name="connect_with_contact_name_question">Conectar com %1$s?</string>
<string name="audio_device_earpiece">Auricular</string>
<string name="devices">Dispositivos</string>
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Abrir <i>Usar a partir de computador</i> na aplicação mobile e ler código QR.]]></string>
<string name="you_sent_group_invitation">Você enviou um convite de grupo</string>
<string name="snd_group_event_changed_role_for_yourself">você mudou o seu cargo para %s</string>
<string name="snd_conn_event_switch_queue_phase_completed_for_member">você mudou o endereço para %s</string>
<string name="v5_3_new_interface_languages">6 novos idiomas de interface</string>
<string name="desktop_app_version_is_incompatible">A versão %s da aplicação de computador não é compatível com esta aplicação.</string>
<string name="remote_host_disconnected_from"><![CDATA[Desconectado do dispositivo móvel <b>%s</b> com a razão: %s]]></string>
<string name="connect__your_profile_will_be_shared">O seu perfil %1$s vai ser partilhado.</string>
<string name="download_file">Transferir</string>
<string name="abort_switch_receiving_address_confirm">Cancelar</string>
<string name="developer_options_section">Opções de programador</string>
<string name="enable_receipts_all">Ativar</string>
<string name="desktop_device">Computador</string>
<string name="contact_sent_large_file">O seu contacto enviou um ficheiro que é maior que o tamanho máximo suportado atualmente (%1$s).</string>
<string name="smp_servers_your_server">O seu servidor</string>
<string name="smp_servers_your_server_address">O endereço do seu servidor</string>
<string name="your_calls">As tuas chamadas</string>
<string name="disable_sending_recent_history">Não enviar histórico a novos membros.</string>
<string name="non_content_uri_alert_text">Você partilhou um caminho de ficheiro inválido. Relate o problema aos programadores da aplicação.</string>
<string name="you_have_no_chats">Você não tem conversas</string>
<string name="create_group_button">Criar grupo</string>
<string name="group_main_profile_sent">O seu perfil de conversa será enviado para os membros do grupo</string>
<string name="remote_ctrl_error_disconnected">Desktop foi disconectado</string>
<string name="migrate_to_device_download_failed">Transferência falhada</string>
<string name="disable_notifications_button">Desativar notificações</string>
<string name="open_simplex_chat_to_accept_call">Abir SimpleX Chat para aceitar a chamada</string>
<string name="receipts_contacts_enable_keep_overrides">Ativar (manter sobreposições)</string>
<string name="info_row_created_at">Criado em</string>
<string name="feature_enabled_for">Ativo para</string>
<string name="v4_3_irreversible_message_deletion_desc">Os seus contactos podem permitir eliminação total de mensagens.</string>
<string name="remote_ctrl_disconnected_with_reason">Desconectado com a razão: %s</string>
<string name="connect_plan_you_have_already_requested_connection_via_this_address">Você já solicitou ligação através deste endereço!</string>
<string name="opensource_protocol_and_code_anybody_can_run_servers">Código e protocolo open-source - qualquer indivíduo pode ser anfitrião dos servidores.</string>
<string name="blocked_items_description">%d mensagens bloqueadas</string>
<string name="marked_deleted_items_description">%d mensagens marcadas como eliminadas</string>
<string name="la_could_not_be_verified">Você não pode ser verificado; por favor tente novamente.</string>
<string name="rcv_direct_event_contact_deleted">contacto eliminado</string>
<string name="abort_switch_receiving_address_question">Cancelar mudança de endereço?</string>
<string name="you_invited_a_contact">Você convidou este contacto</string>
<string name="correct_name_to">Corrigir nome para %s?</string>
<string name="discover_on_network">Descobrir na rede local</string>
<string name="receipts_groups_title_enable">Ativar recibos para grupos?</string>
<string name="receipts_contacts_disable_for_all">Desativar para todos</string>
<string name="snd_group_event_member_unblocked">Você desbloqueou %s</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Você será conectado quando o pedido de ligação for aceite, por favor aguarde ou volte mais tarde!</string>
<string name="your_current_profile">O seu perfil atual</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">O seu perfil será armazenado no seu dispositivo e partilhado apenas com os seus contactos. Os servidores SimpleX não conseguem ver o seu perfil.</string>
<string name="you_must_use_the_most_recent_version_of_database">Você tem que usar a versão mais recente da sua base de dados de conversas em APENAS UM dispositivo, caso contrário poderá deixar de receber mensagens de alguns contactos.</string>
<string name="database_will_be_encrypted_and_passphrase_stored_in_settings">A base de dados será encriptada e a palavra-passe armazenada nas definições.</string>
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Você juntou-se a este group. A conectar ao membro que o convidou.</string>
<string name="snd_group_event_changed_member_role">você mudou o cargo de %s para %s</string>
<string name="profile_update_event_contact_name_changed">contacto %1$s mudou para %2$s</string>
<string name="snd_conn_event_switch_queue_phase_completed">você mudou de endereço</string>
<string name="send_receipts_disabled">desativado</string>
<string name="share_text_created_at">Criado em: %s</string>
<string name="member_info_member_disabled">desativado</string>
<string name="v5_3_discover_join_groups">Descobrir e juntar a grupos</string>
<string name="desktop_devices">Dispositivos desktop</string>
<string name="acknowledged">Compreendido</string>
<string name="decryption_errors">errors de desencriptação</string>
<string name="connections">Ligações</string>
<string name="created">Criado</string>
<string name="deleted">Eliminado</string>
<string name="deletion_errors">Erros de eliminação</string>
<string name="copy_error">Erro de cópia</string>
<string name="v5_3_new_desktop_app_descr">Criar novo perfil na aplicação de computador. 💻</string>
<string name="delivery_receipts_title">Recibos de entrega!</string>
<string name="delivery_receipts_are_disabled">Recibos de entrega desativados!</string>
<string name="dont_enable_receipts">Não ativar</string>
<string name="observer_cant_send_message_title">Você não pode enviar mensagens!</string>
<string name="delete_and_notify_contact">Eliminar e notificar contacto</string>
<string name="delete_without_notification">Eliminar sem notificação</string>
<string name="deleted_chats">Conversas eliminadas</string>
<string name="your_simplex_contact_address">O seu endereço SimpleX</string>
<string name="contact_list_header_title">Os seus contactos</string>
<string name="your_chat_profiles">Os seus perfis de conversa</string>
<string name="you_can_view_invitation_link_again">Você pode ver a ligação de convite outra vez nos detalhes da ligação.</string>
<string name="app_check_for_updates_download_started">A transferir atualização da app, não feche a aplicação</string>
<string name="app_check_for_updates_button_download">Transferir %s (%s)</string>
<string name="your_ICE_servers">Os seus servidores ICE</string>
<string name="your_profile_is_stored_on_your_device">O seu perfil, contactos e mensagens entregues são armazenados no seu dispositivo.</string>
<string name="your_ice_servers">Os seus servidores ICE</string>
<string name="receipts_groups_disable_keep_overrides">Desativar (manter as sobreposições de grupo)</string>
<string name="receipts_contacts_disable_keep_overrides">Desativar (manter sobreposições)</string>
<string name="receipts_contacts_title_disable">Desativar recibos?</string>
<string name="receipts_groups_title_disable">Desativar recibos para grupos?</string>
<string name="receipts_contacts_enable_for_all">Ativar para todos</string>
<string name="receipts_groups_enable_for_all">Ativar para todos os grupos</string>
<string name="receipts_contacts_title_enable">Ativar recibos?</string>
<string name="you_rejected_group_invitation">Você rejeitou o convite de grupo</string>
<string name="you_joined_this_group">Você juntou-se a este grupo</string>
<string name="rcv_group_events_count">%d eventos de grupo</string>
<string name="snd_conn_event_ratchet_sync_ok">encriptação ok para %s</string>
<string name="connect_via_member_address_alert_desc">O pedido de ligação vai ser enviado para este membro do grupo.</string>
<string name="cant_call_contact_deleted_alert_text">O contacto foi eliminado.</string>
<string name="you_need_to_allow_calls">Você precisa de permitir que o seu contacto inicie chamada de voz para poder iniciar uma chamada de voz.</string>
<string name="color_mode_dark">Escuro</string>
<string name="dark_mode_colors">Cores de modo escuro</string>
<string name="chat_theme_apply_to_dark_mode">Modo escuro</string>
<string name="appearance_zoom">Aproximar</string>
<string name="remote_ctrl_error_inactive">Desktop está inativo</string>
<string name="current_user">Perfil atual</string>
<string name="migrate_from_device_delete_database_from_device">Eliminar base de dados deste dispositivo</string>
<string name="servers_info_detailed_statistics">Estatísticas detalhadas</string>
<string name="servers_info_downloaded">Transferido</string>
<string name="downloaded_files">Ficheiros transferidos</string>
<string name="download_errors">Erros de transferência</string>
<string name="connect_plan_connect_via_link">Conectar via link?</string>
<string name="create_another_profile_button">Criar perfil</string>
<string name="creating_link">A criar ligação…</string>
<string name="contact_deleted">Contacto eliminado!</string>
<string name="conversation_deleted">Conversa eliminada!</string>
<string name="agent_critical_error_title">Erro crítico</string>
<string name="this_device_name">Nome deste dispositivo</string>
<string name="delete_contact_cannot_undo_warning">O contacto será eliminado - esta ação é irreversível!</string>
<string name="database_encryption_will_be_updated_in_settings">A palavra-passe de encriptação da base de dados será atualizada e armazenada nas definições.</string>
<string name="v5_4_incognito_groups_descr">Criar um grupo utilizando um perfil aleatório.</string>
<string name="database_migration_in_progress">Migração da base de dados em progresso.
\nPode demorar alguns minutos.</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">Você será conectado quando o dispositivo do seu contacto estiver online, por favor aguarde ou volte mais tarde!</string>
<string name="disconnect_remote_host">Desconectar</string>
<string name="app_check_for_updates_disabled">Desativado</string>
<string name="your_XFTP_servers">Os seus servidores XFTP</string>
<string name="you_control_your_chat">Você controla a conversa!</string>
<string name="you_can_still_view_conversation_with_contact">Você poderá ver a conversa com %1$s na lista de conversas.</string>
<string name="your_SMP_servers">Os seus servidores SMP</string>
<string name="you_can_use_markdown_to_format_messages__prompt">Você pode usar markdown para formatar mensagens:</string>
<string name="you_can_still_send_messages_to_contact">Você poderá enviar mensagens para %1$s através das conversas Eliminadas.</string>
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">O seu contacto precisa de estar online para a ligação ser completada.
\nVocê pode cancelar esta ligação e remover o contacto (e tentar mais tarde com uma nova ligação).</string>
<string name="your_contacts_will_remain_connected">Os seus contactos permanecerão conectados.</string>
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Você terá que autenticar-se quando iniciar ou abrir a aplicação após 30 segundos em segundo plano.</string>
<string name="enable_camera_access">Ativar acesso à câmara</string>
<string name="you_need_to_allow_to_send_voice">Você precisa de permitir que o seu contacto envie mensagens de aúdio para poder enviá-las.</string>
<string name="app_check_for_updates_notice_disable">Desativar</string>
<string name="receipts_groups_disable_for_all">Desativar para todos os grupos</string>
<string name="your_chat_database">A tua base de dados de conversas</string>
<string name="servers_info_details">Detalhes</string>
<string name="you_can_share_this_address_with_your_contacts">Você pode partilhar o seu endereço com os seus contactos para permitir que se conectem com %s.</string>
<string name="you_can_share_your_address">Você pode partilhar o seu endereço como uma ligação ou código QR - qualquer pessoa pode conectar-se a si.</string>
</resources>
@@ -193,7 +193,6 @@
<string name="reject">Respinge</string>
<string name="save_passphrase_in_settings">Salvează fraza de acces în setări</string>
<string name="save_and_update_group_profile">Salvează și actualizează profilul grupului</string>
<string name="network_options_revert">Revenire</string>
<string name="connect_plan_repeat_join_request">Repetă cererea de alăturare?</string>
<string name="restart_chat_button">Repornește conversația</string>
<string name="saved_description">salvat</string>
@@ -394,7 +394,6 @@
<string name="network_enable_socks_info">Соединяться с серверами через SOCKS прокси через порт %d? Прокси должен быть запущен до включения этой опции.</string>
<string name="network_disable_socks">Использовать прямое соединение с Интернет?</string>
<string name="network_disable_socks_info">Если Вы подтвердите, серверы смогут видеть Ваш IP адрес, а провайдер - с какими серверами Вы соединяетесь.</string>
<string name="update_onion_hosts_settings_question">Обновить настройки .onion хостов?</string>
<string name="network_use_onion_hosts">Использовать .onion хосты</string>
<string name="network_use_onion_hosts_prefer">Когда возможно</string>
<string name="network_use_onion_hosts_no">Нет</string>
@@ -403,9 +402,6 @@
<string name="network_use_onion_hosts_no_desc">Onion хосты не используются.</string>
<string name="network_use_onion_hosts_required_desc">Подключаться только к onion хостам.
\nОбратите внимание: Вы не сможете соединиться с серверами, у которых нет .onion адреса.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion хосты используются, если возможно.</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion хосты не используются.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Подключаться только к onion хостам.</string>
<string name="appearance_settings">Интерфейс</string>
<!-- Address Items - UserAddressView.kt -->
<string name="create_address">Создать адрес</string>
@@ -831,7 +827,6 @@
<string name="network_option_protocol_timeout">Таймаут протокола</string>
<string name="network_option_ping_interval">Интервал PING</string>
<string name="network_option_enable_tcp_keep_alive">Включить TCP keep-alive</string>
<string name="network_options_revert">Отменить изменения</string>
<string name="network_options_save">Сохранить</string>
<string name="update_network_settings_question">Обновить настройки сети?</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">Обновление настроек приведет к переподключению клиента ко всем серверам.</string>
@@ -1941,4 +1936,22 @@
<string name="color_wallpaper_tint">Рисунок обоев</string>
<string name="color_wallpaper_background">Фон обоев</string>
<string name="chat_theme_apply_to_mode">Применить к</string>
<string name="cannot_share_message_alert_title">Не удается отправить сообщение</string>
<string name="app_check_for_updates_beta">Бета</string>
<string name="servers_info_sessions_connected">Соединeно</string>
<string name="attempts_label">попытки</string>
<string name="completed">Готово</string>
<string name="confirm_delete_contact_question">Потвердить удаление контакта?</string>
<string name="privacy_media_blur_radius">Размытие изображения</string>
<string name="all_users">Все профили</string>
<string name="app_check_for_updates_notice_title">Проверка на наличиее обновлений</string>
<string name="allow_calls_question">Разрешить звонки?</string>
<string name="calls_prohibited_alert_title">Звонки запрещены!</string>
<string name="cant_call_member_alert_title">Не удается позвонить члену группы</string>
<string name="app_check_for_updates_download_completed_title">Обновление скачано</string>
<string name="info_view_call_button">звонок</string>
<string name="cant_call_contact_alert_title">Не удается позвонить контакту</string>
<string name="cant_send_message_to_member_alert_title">Не удается написать члену группы</string>
<string name="app_check_for_updates">Проверка на наличиее обновлений</string>
<string name="info_view_connect_button">соединиться</string>
</resources>
@@ -665,10 +665,7 @@
<string name="network_settings_title">การตั้งค่าเครือข่าย</string>
<string name="network_use_onion_hosts_required_desc">จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ</string>
<string name="network_use_onion_hosts_no">ไม่</string>
<string name="network_use_onion_hosts_required_desc_in_alert">จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">โฮสต์หัวหอมจะถูกใช้เมื่อมี</string>
<string name="network_use_onion_hosts_no_desc">โฮสต์หัวหอมจะไม่ถูกใช้</string>
<string name="network_use_onion_hosts_no_desc_in_alert">โฮสต์หัวหอมจะไม่ถูกใช้</string>
<string name="password_to_show">รหัสผ่านที่จะแสดง</string>
<string name="callstatus_missed">สายที่ไม่ได้รับ</string>
<string name="people_can_connect_only_via_links_you_share">ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น</string>
@@ -852,7 +849,6 @@
<string name="save_and_update_group_profile">บันทึกและอัปเดตโปรไฟล์กลุ่ม</string>
<string name="receiving_via">กำลังรับผ่าน</string>
<string name="network_options_reset_to_defaults">รีเซ็ตเป็นค่าเริ่มต้น</string>
<string name="network_options_revert">เปลี่ยนกลับ</string>
<string name="network_options_save">บันทึก</string>
<string name="reset_color">รีเซ็ตสี</string>
<string name="feature_received_prohibited">ได้รับ, ห้าม</string>
@@ -1147,7 +1143,6 @@
<string name="network_socks_toggle_use_socks_proxy">ใช้พร็อกซี SOCKS</string>
<string name="network_disable_socks">ใช้การเชื่อมต่ออินเทอร์เน็ตโดยตรงหรือไม่\?</string>
<string name="network_enable_socks">ใช้พร็อกซี SOCKS หรือไม่\?</string>
<string name="update_onion_hosts_settings_question">อัปเดตการตั้งค่าโฮสต์ .onion ไหม\?</string>
<string name="network_use_onion_hosts">ใช้โฮสต์ .onion</string>
<string name="network_use_onion_hosts_prefer">เมื่อพร้อมใช้งาน</string>
<string name="update_network_session_mode_question">อัปเดตโหมดการแยกการขนส่งไหม\?</string>
@@ -1145,7 +1145,6 @@
<string name="share_link">Bağlantı paylaş</string>
<string name="icon_descr_simplex_team">SimpleX Ekibi</string>
<string name="rcv_group_event_3_members_connected">%s, %s ve %s bağlandı</string>
<string name="network_options_revert">Geri al</string>
<string name="settings_section_title_socks">SOCKS VEKİLİ</string>
<string name="desktop_devices">Masaüstür cihazlar</string>
<string name="smp_servers">SMP sunucuları</string>
@@ -1318,14 +1317,12 @@
<string name="share_text_updated_at">Kayıt %s te güncellendi</string>
<string name="v5_3_encrypt_local_files_descr">Uygulama yeni yerel dosyaları şifreler (videolar dışında).</string>
<string name="sender_at_ts">%s , %s de</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Bağlantı için Onion ana bilgisayarları gerekli olacaktır.</string>
<string name="receipts_contacts_title_disable">Alıcılar devre dışı bırakılsın mı?</string>
<string name="sync_connection_force_question">Bağlantı yeniden senkronizasyonunu şifrele?</string>
<string name="receipts_section_description_1">Grup ayarlarından ve kişilerden geçersiz kılınmış olabilirler</string>
<string name="recent_history_is_not_sent_to_new_members">Yeni üyelere geçmiş gönderilmedi.</string>
<string name="v4_2_security_assessment">Güvenlik değerlendirmesi</string>
<string name="retry_verb">Yeniden dene</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion ana bilgisayarları mümkün olduğunda kullanılacaktır.</string>
<string name="network_enable_socks_info">Sunuculara %d bağlantı noktasındaki vekil SOCKS aracılığıyla erişilsin mi? Vekil bu seçeneği etkinleştirmeden önce başlatılmak zorundadır.</string>
<string name="v5_4_block_group_members_descr">İstenmeyen mesajları gizlemek için.</string>
<string name="error_smp_test_failed_at_step">Test %s adımında hata yaşandı.</string>
@@ -1362,7 +1359,6 @@
<string name="remove_passphrase_from_settings">Ayarlardaki parola silinsin mi?</string>
<string name="receipts_contacts_title_enable">Alıcılar etkinleştirilsin mi?</string>
<string name="tap_to_start_new_chat">Yeni bir sohbet başlatmak için tıkla</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion ana bilgisayarları kullanılmayacaktır.</string>
<string name="unblock_member_button">Kişinin engelini kaldır</string>
<string name="relay_server_if_necessary">Yönlendirici sunucusu sadece lazım ise kullanılacak. Diğer taraf IP adresini görebilir.</string>
<string name="remote_host_was_disconnected_toast"><![CDATA[Telefon bağlantılı <b>%s</b> ın bağlantısı kesildi]]></string>
@@ -1510,7 +1506,6 @@
<string name="connected_to_mobile">Telefona bağlandı</string>
<string name="v5_3_simpler_incognito_mode_descr">Bağlanırken takma ada geçiş yap.</string>
<string name="v5_2_message_delivery_receipts">Mesaj gönderildi!</string>
<string name="update_onion_hosts_settings_question">.onion ana bilgisayarları ayarı güncellensin mi?</string>
<string name="smp_servers_use_server_for_new_conn">Yeni bağlantılar için kullan</string>
<string name="rcv_conn_event_switch_queue_phase_completed">senin için adres değiştirildi</string>
<string name="network_enable_socks">SOCKS vekilini kullan?</string>
@@ -251,7 +251,6 @@
<string name="network_settings_title">Налаштування мережі</string>
<string name="network_enable_socks">Використовувати SOCKS-проксі?</string>
<string name="network_disable_socks">Використовувати прямий підключення до Інтернету?</string>
<string name="update_onion_hosts_settings_question">Оновити налаштування .onion-хостів?</string>
<string name="network_use_onion_hosts">Використовувати .onion-хости</string>
<string name="network_use_onion_hosts_prefer">Якщо доступно</string>
<string name="network_use_onion_hosts_no">Ні</string>
@@ -640,8 +639,6 @@
<string name="error_saving_ICE_servers">Помилка збереження серверів ICE</string>
<string name="network_use_onion_hosts_required_desc">.Onion-хости будуть обов\'язковими для підключення.
\nЗверніть увагу: ви не зможете підключитися до серверів без адреси .onion.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Хости .onion будуть використовуватися, якщо доступні.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Хости .onion будуть обов\'язковими для підключення.</string>
<string name="show_developer_options">Показати параметри розробника</string>
<string name="developer_options">Ідентифікатори бази даних та опція ізоляції транспорту.</string>
<string name="shutdown_alert_desc">Сповіщення перестануть працювати, поки ви не перезапустите додаток</string>
@@ -953,7 +950,6 @@
<string name="image_descr_link_preview">зображення попереднього перегляду посилання</string>
<string name="icon_descr_cancel_link_preview">скасувати попередній перегляд посилання</string>
<string name="icon_descr_settings">Налаштування</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Хости .onion не будуть використовуватися.</string>
<string name="icon_descr_call_progress">Виклик у процесі</string>
<string name="database_error">Помилка бази даних</string>
<string name="restore_database_alert_confirm">Відновити</string>
@@ -997,7 +993,6 @@
<string name="section_title_for_console">ДЛЯ КОНСОЛІ</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">Учасника буде вилучено з групи - цю дію неможливо скасувати!</string>
<string name="change_role">Змінити роль</string>
<string name="network_options_revert">Відновити</string>
<string name="you_will_still_receive_calls_and_ntfs">Ви все ще отримуватимете дзвінки та сповіщення від приглушених профілів, коли вони активні.</string>
<string name="ttl_months">%d місяці</string>
<string name="ttl_mth">%dmth</string>
@@ -542,4 +542,19 @@
<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>
<string name="info_view_call_button">gọi</string>
<string name="info_view_connect_button">kết nối</string>
<string name="delete_contact_cannot_undo_warning">Liên hệ sẽ bị xóa - điều này không thể hoàn tác!</string>
<string name="conversation_deleted">Cuộc trò chuyện đã bị xóa!</string>
<string name="privacy_media_blur_radius">Làm mờ phương tiện truyền thông</string>
<string name="cant_call_contact_alert_title">Không thể gọi liên hệ</string>
<string name="allow_calls_question">Cho phép thực hiện cuộc gọi?</string>
<string name="calls_prohibited_alert_title">Cuộc gọi bị cấm!</string>
<string name="cant_call_contact_connecting_wait_alert_text">Đang kết nối tới liên hệ, xin vui lòng đợi hoặc kiểm tra sau!</string>
<string name="cant_call_contact_deleted_alert_text">Liên hệ đã bị xóa.</string>
<string name="cant_send_message_to_member_alert_title">Không thể nhắn tin cho thành viên nhóm</string>
<string name="cant_call_member_alert_title">Không thể gọi thành viên nhóm</string>
<string name="confirm_delete_contact_question">Xác nhận xóa liên hệ?</string>
<string name="contact_deleted">Liên hệ đã bị xóa!</string>
<string name="deleted_chats">Đã xóa các cuộc trò chuyện</string>
</resources>
@@ -667,7 +667,6 @@
<string name="feature_off">关闭</string>
<string name="network_use_onion_hosts_required_desc">连接需要 Onion 主机。
\n请注意:如果没有 .onion 地址,您将无法连接到服务器。</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion 主机将在可用时使用。</string>
<string name="chat_item_ttl_none">从不</string>
<string name="feature_offered_item">已提供 %s</string>
<string name="feature_offered_item_with_param">已提供 %s:%2s</string>
@@ -686,8 +685,6 @@
<string name="no_contacts_to_add">没有联系人可添加</string>
<string name="network_status">网络状态</string>
<string name="chat_preferences_off">关闭</string>
<string name="network_use_onion_hosts_no_desc_in_alert">将不会使用 Onion 主机。</string>
<string name="network_use_onion_hosts_required_desc_in_alert">连接需要 Onion 主机。</string>
<string name="no_received_app_files">没有收到或发送的文件</string>
<string name="sender_cancelled_file_transfer">发送人已取消文件传输。</string>
<string name="share_verb">分享</string>
@@ -744,7 +741,6 @@
<string name="to_start_a_new_chat_help_header">开始新的聊天</string>
<string name="to_verify_compare">要与您的联系人验证端到端加密,请比较(或扫描)您设备上的代码。</string>
<string name="unmute_chat">取消静音</string>
<string name="update_onion_hosts_settings_question">更新 .onion 主机设置?</string>
<string name="update_network_session_mode_question">更新传输隔离模式?</string>
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(从剪贴板扫描或粘贴)</string>
<string name="smp_server_test_secure_queue">保护队列</string>
@@ -829,7 +825,6 @@
<string name="group_member_status_removed">已删除</string>
<string name="role_in_group">角色</string>
<string name="network_option_seconds_label">秒</string>
<string name="network_options_revert">恢复</string>
<string name="reset_color">重置颜色</string>
<string name="v4_5_reduced_battery_usage">减少电池使用量</string>
<string name="v4_5_private_filenames_descr">为了保护时区,图像/语音文件使用 UTC。</string>
@@ -1107,10 +1102,10 @@
<string name="v5_0_large_files_support">最大 1gb 的视频和文件</string>
<string name="v5_0_large_files_support_descr">快速且无需等待发件人在线!</string>
<string name="prohibit_calls">禁止音频/视频通话。</string>
<string name="both_you_and_your_contact_can_make_calls">您和您的联系人都可以拨打电话。</string>
<string name="only_you_can_make_calls">只有您可以拨打电话。</string>
<string name="only_your_contact_can_make_calls">只有您的联系人可以拨打电话。</string>
<string name="allow_your_contacts_to_call">允许您的联系人与您进行语音通话。</string>
<string name="both_you_and_your_contact_can_make_calls">您和您的联系人都可以进行呼叫。</string>
<string name="only_you_can_make_calls">只有您可以进行呼叫。</string>
<string name="only_your_contact_can_make_calls">只有您的联系人可以进行呼叫。</string>
<string name="allow_your_contacts_to_call">允许联系人呼叫你。</string>
<string name="allow_calls_only_if">允许通话,前提是你的联系人允许它们。</string>
<string name="calls_prohibited_with_this_contact">禁止音频/视频通话。</string>
<string name="send_disappearing_message_1_minute">1分钟</string>
@@ -2000,4 +1995,44 @@
<string name="privacy_media_blur_radius_off">关闭</string>
<string name="privacy_media_blur_radius_soft">轻柔</string>
<string name="privacy_media_blur_radius_strong">强烈</string>
<string name="info_view_message_button">消息</string>
<string name="info_view_call_button">呼叫</string>
<string name="delete_contact_cannot_undo_warning">联系人将被删除 - 无法撤销此操作!</string>
<string name="keep_conversation">保留对话</string>
<string name="only_delete_conversation">只删除对话</string>
<string name="contact_deleted">删除了联系人!</string>
<string name="conversation_deleted">删除了对话!</string>
<string name="delete_without_notification">不通知删除</string>
<string name="you_can_still_view_conversation_with_contact">你仍可以在聊天列表中查看与 %1$s 的对话。</string>
<string name="paste_link">粘贴链接</string>
<string name="contact_list_header_title">联系人</string>
<string name="one_hand_ui">单手用户界面</string>
<string name="cant_call_contact_connecting_wait_alert_text">正在连接联系人,请等候或稍后检查!</string>
<string name="cant_call_contact_deleted_alert_text">联系人被删除了。</string>
<string name="you_need_to_allow_calls">要能够呼叫联系人,你需要先允许联系人进行呼叫。</string>
<string name="calls_prohibited_ask_to_enable_calls_alert_text">请让你的联系人启用通话。</string>
<string name="cant_call_contact_alert_title">无法呼叫联系人</string>
<string name="cant_call_member_alert_title">无法呼叫群成员</string>
<string name="cant_send_message_to_member_alert_title">无法给群成员发消息</string>
<string name="confirm_delete_contact_question">确认删除联系人?</string>
<string name="info_view_connect_button">连接</string>
<string name="action_button_add_members">邀请</string>
<string name="no_filtered_contacts">没有过滤的联系人</string>
<string name="info_view_open_button">打开</string>
<string name="info_view_search_button">搜索</string>
<string name="cant_call_member_send_message_alert_text">发送消息来开启通话。</string>
<string name="you_can_still_send_messages_to_contact">你仍可以从已删除的聊天给 %1$s 发送消息。</string>
<string name="deleted_chats">已删除的聊天</string>
<string name="allow_calls_question">允许通话?</string>
<string name="calls_prohibited_alert_title">通话被禁止!</string>
<string name="toolbar_settings">设置</string>
<string name="info_view_video_button">视频</string>
<string name="delete_messages_mark_deleted_warning">消息将被标记为删除。收信人将可以揭示这些消息。</string>
<string name="delete_members_messages__question">删除成员的 %d 条消息吗?</string>
<string name="select_verb">选择</string>
<string name="compose_message_placeholder">写消息</string>
<string name="selected_chat_items_nothing_selected">什么也没选中</string>
<string name="selected_chat_items_selected_n">已选中 %d</string>
<string name="moderate_messages_will_be_deleted_warning">将对所有成员删除这些消息。</string>
<string name="moderate_messages_will_be_marked_warning">这些消息将对所有成员标记为受管制。</string>
</resources>
@@ -412,8 +412,6 @@
<string name="enter_one_ICE_server_per_line">ICE 伺服器(每行一個)</string>
<string name="network_use_onion_hosts">使用 .onion 主機</string>
<string name="network_use_onion_hosts_no_desc">Onion 主機不會啟用。</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion 主機會在可用時啟用。</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion 主機不會啟用。</string>
<string name="network_session_mode_entity">連接</string>
<string name="delete_address">刪除聯絡地址</string>
<string name="colored_text">顏色</string>
@@ -437,7 +435,6 @@
<string name="ensure_ICE_server_address_are_correct_format_and_unique">請確保你的 WebRTC ICE 伺服器地址是正確的格式,每行也有分隔和沒有重複。</string>
<string name="network_disable_socks">使用直接互聯網連接?</string>
<string name="network_disable_socks_info">如果你確定,你的訊息伺服器能夠看到你的 IP 位置,和你的網路供應商 - 你正在連接到哪些伺服器。</string>
<string name="update_onion_hosts_settings_question">更新 .onion 主機設定?</string>
<string name="delete_image">刪除圖片</string>
<string name="callstate_starting">開始中 …</string>
<string name="callstate_waiting_for_answer">等待對方回應…</string>
@@ -496,7 +493,6 @@
<string name="network_use_onion_hosts_no">不</string>
<string name="network_use_onion_hosts_required">需要</string>
<string name="network_use_onion_hosts_prefer_desc">Onion 主機會在可用時啟用。</string>
<string name="network_use_onion_hosts_required_desc_in_alert">連接時將需要使用 Onion 主機。</string>
<string name="delete_address__question">刪除聯絡地址?</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">你的個人檔案只會儲存於你的裝置和只會分享給你的聯絡人。 SimpleX 伺服器並不會看到你的個人檔案。</string>
<string name="save_and_notify_contact">儲存並通知你的聯絡人</string>
@@ -825,7 +821,6 @@
<string name="disappearing_messages_are_prohibited">自動銷毀訊息於這個群組內是禁用的。</string>
<string name="feature_offered_item">已提供 %s</string>
<string name="error_saving_group_profile">儲存群組檔案時出錯</string>
<string name="network_options_revert">恢復</string>
<string name="theme">主題</string>
<string name="chat_preferences_you_allow">你允許</string>
<string name="set_group_preferences">修改群組內的設定</string>
@@ -1,9 +1,7 @@
package chat.simplex.common.platform
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
@@ -17,8 +15,7 @@ import androidx.compose.ui.input.key.*
import androidx.compose.ui.platform.*
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.*
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import chat.simplex.common.views.chat.*
@@ -49,6 +46,7 @@ actual fun PlatformTextField(
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean,
placeholder: String,
onMessageChange: (String) -> Unit,
onUpArrow: () -> Unit,
onFilesPasted: (List<URI>) -> Unit,
@@ -58,7 +56,7 @@ actual fun PlatformTextField(
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
val keyboard = LocalSoftwareKeyboardController.current
val padding = PaddingValues(12.dp, 12.dp, 45.dp, 0.dp)
val padding = PaddingValues(0.dp, 12.dp, 50.dp, 0.dp)
LaunchedEffect(cs.contextItem) {
if (cs.contextItem !is ComposeContextItem.QuotedItem) return@LaunchedEffect
// In replying state
@@ -169,9 +167,20 @@ actual fun PlatformTextField(
CompositionLocalProvider(
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current
) {
Column(Modifier.weight(1f).padding(start = 12.dp, end = 32.dp)) {
Column(Modifier.weight(1f).padding(start = 0.dp, end = 50.dp)) {
Spacer(Modifier.height(8.dp))
innerTextField()
TextFieldDefaults.TextFieldDecorationBox(
value = textFieldValue.text,
innerTextField = innerTextField,
placeholder = { Text(placeholder, style = textStyle.value.copy(color = MaterialTheme.colors.secondary)) },
singleLine = false,
enabled = true,
isError = false,
trailingIcon = null,
interactionSource = remember { MutableInteractionSource() },
contentPadding = PaddingValues(),
visualTransformation = VisualTransformation.None,
)
Spacer(Modifier.height(10.dp))
}
}
@@ -35,7 +35,6 @@ actual fun ChatListNavLinkLayout(
disabled: Boolean,
selectedChat: State<Boolean>,
nextChatSelected: State<Boolean>,
oneHandUI: State<Boolean>,
) {
var modifier = Modifier.fillMaxWidth()
if (!disabled) modifier = modifier