${link.format.simplexLinkText}"
- else
- ""
+ val target = strConnectTarget(shortOrFullLink.trim())
+ val linkText = if (target is ConnectTarget.Link) "
${target.linkText}" else ""
when (connectionPlan) {
is ConnectionPlan.InvitationLink -> when (connectionPlan.invitationLinkPlan) {
is InvitationLinkPlan.Ok ->
@@ -316,6 +322,33 @@ private suspend fun planAndConnectTask(
cleanup()
}
}
+ is GroupLinkPlan.UpdateRequired -> {
+ Log.d(TAG, "planAndConnect, .GroupLink, .UpdateRequired")
+ val groupSLinkData = connectionPlan.groupLinkPlan.groupSLinkData_
+ if (groupSLinkData != null) {
+ AlertManager.privacySensitive.showOpenChatAlert(
+ profileName = groupSLinkData.groupProfile.displayName,
+ profileFullName = groupSLinkData.groupProfile.fullName,
+ profileImage = {
+ ProfileImage(
+ size = alertProfileImageSize,
+ image = groupSLinkData.groupProfile.image,
+ icon = MR.images.ic_supervised_user_circle_filled
+ )
+ },
+ subtitle = generalGetString(MR.strings.group_link_requires_newer_version),
+ confirmText = null,
+ dismissText = generalGetString(MR.strings.ok),
+ onDismiss = { cleanup() }
+ )
+ } else {
+ AlertManager.privacySensitive.showAlertMsg(
+ generalGetString(MR.strings.app_update_required),
+ generalGetString(MR.strings.group_link_requires_newer_version)
+ )
+ cleanup()
+ }
+ }
}
is ConnectionPlan.Error -> {
Log.d(TAG, "planAndConnect, error ${connectionPlan.chatError}")
@@ -441,6 +474,8 @@ private fun showOpenKnownContactAlert(chatModel: ChatModel, rhId: Long?, close:
icon = contact.chatIconName
)
},
+ // the alert shows the badge inline, so it skips the long-expired (ExpiredOld) badge here too
+ profileBadge = if (contact.active && contact.profile.localBadge?.status != BadgeStatus.ExpiredOld) contact.profile.localBadge else null,
confirmText = generalGetString(if (contact.nextConnectPrepared) MR.strings.connect_plan_open_new_chat else MR.strings.connect_plan_open_chat),
onConfirm = {
openKnownContact(chatModel, rhId, close, contact)
@@ -600,6 +635,7 @@ fun showPrepareContactAlert(
else MR.images.ic_account_circle_filled
)
},
+ profileBadge = if (contactShortLinkData.localBadge?.status == BadgeStatus.ExpiredOld) null else contactShortLinkData.localBadge,
information = ownerVerificationMessage(ownerVerification),
confirmText = generalGetString(MR.strings.connect_plan_open_new_chat),
onConfirm = {
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt
index 0f299b5187..f1bd732d87 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt
@@ -7,6 +7,7 @@ import SectionView
import SectionViewWithButton
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.background
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -130,7 +131,7 @@ private fun ContactConnectionInfoLayout(
if (connLink != null && connLink.connFullLink.isNotEmpty() && contactConnection.initiated) {
Spacer(Modifier.height(DEFAULT_PADDING))
SectionViewWithButton(
- stringResource(MR.strings.one_time_link).uppercase(),
+ stringResource(MR.strings.one_time_link),
titleButton = if (connLink.connShortLink == null) null else {{ ToggleShortLinkButton(showShortLink) }}
) {
SimpleXCreatedLinkQRCode(connLink, short = showShortLink.value)
@@ -146,7 +147,7 @@ private fun ContactConnectionInfoLayout(
}
SectionTextFooter(sharedProfileInfo(chatModel, contactConnection.incognito))
- SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
+ SectionDividerSpaced()
DeleteButton(deleteConnection)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt
index 1eceaf4158..af7f59496b 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt
@@ -325,7 +325,7 @@ private fun ModalData.NewChatSheetLayout(
item {
if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) {
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
- SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {}
+ SectionView(stringResource(MR.strings.contact_list_header_title), headerBottomPadding = DEFAULT_PADDING_HALF) {}
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
}
}
@@ -410,7 +410,7 @@ private fun ModalData.NewChatSheetLayout(
item {
if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) {
SectionDividerSpaced()
- SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {}
+ SectionView(stringResource(MR.strings.contact_list_header_title), headerBottomPadding = DEFAULT_PADDING_HALF) {}
}
}
item {
@@ -523,34 +523,32 @@ private fun ContactsSearchBar(
snapshotFlow { searchText.value.text }
.distinctUntilChanged()
.collect {
- val link = strHasSingleSimplexLink(it.trim())
- if (link != null) {
- // if SimpleX link is pasted, show connection dialogue
- hideKeyboard(view)
- if (link.format is Format.SimplexLink) {
- val linkText = link.format.simplexLinkText
- searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero)
+ when (val target = strConnectTarget(it.trim())) {
+ is ConnectTarget.Link -> {
+ hideKeyboard(view)
+ searchText.value = searchText.value.copy(target.linkText, selection = TextRange.Zero)
+ searchShowingSimplexLink.value = true
+ searchChatFilteredBySimplexLink.value = null
+ connect(
+ link = target.text,
+ searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
+ close = close,
+ cleanup = { searchText.value = TextFieldValue() }
+ )
}
- searchShowingSimplexLink.value = true
- searchChatFilteredBySimplexLink.value = null
- connect(
- link = link.text,
- searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
- close = close,
- cleanup = { searchText.value = TextFieldValue() }
- )
- } else if (!searchShowingSimplexLink.value || it.isEmpty()) {
- if (it.isNotEmpty()) {
- // if some other text is pasted, enter search mode
- focusRequester.requestFocus()
- } else {
- connectProgressManager.cancelConnectProgress()
- if (listState.layoutInfo.totalItemsCount > 0) {
- listState.scrollToItem(0)
+ is ConnectTarget.Name -> showUnsupportedNameAlert(target.nameInfo)
+ null -> if (!searchShowingSimplexLink.value || it.isEmpty()) {
+ if (it.isNotEmpty()) {
+ focusRequester.requestFocus()
+ } else {
+ connectProgressManager.cancelConnectProgress()
+ if (listState.layoutInfo.totalItemsCount > 0) {
+ listState.scrollToItem(0)
+ }
}
+ searchShowingSimplexLink.value = false
+ searchChatFilteredBySimplexLink.value = null
}
- searchShowingSimplexLink.value = false
- searchChatFilteredBySimplexLink.value = null
}
}
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt
index 72311cd7fe..6799fa1300 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt
@@ -230,7 +230,8 @@ private fun ProfilePickerOption(
disabled: Boolean,
onSelected: () -> Unit,
image: @Composable () -> Unit,
- onInfo: (() -> Unit)? = null
+ onInfo: (() -> Unit)? = null,
+ badge: LocalBadge? = null
) {
Row(
Modifier
@@ -243,7 +244,7 @@ private fun ProfilePickerOption(
) {
image()
TextIconSpaced(false)
- Text(title, modifier = Modifier.align(Alignment.CenterVertically))
+ NameWithBadge(title, badge, Modifier.align(Alignment.CenterVertically))
if (onInfo != null) {
Spacer(Modifier.padding(6.dp))
Column(Modifier
@@ -365,7 +366,8 @@ fun ActiveProfilePicker(
}
}
},
- image = { ProfileImage(size = 42.dp, image = user.image) }
+ image = { ProfileImage(size = 42.dp, image = user.image) },
+ badge = user.profile.localBadge
)
}
@@ -495,7 +497,7 @@ private fun InviteView(rhId: Long?, connLinkInvitation: CreatedConnLink, contact
)
SimpleXCreatedLinkQRCode(connLinkInvitation, short = showShortLink.value, onShare = { chatModel.markShowingInvitationUsed() })
} else {
- SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase(), headerBottomPadding = 5.dp) {
+ SectionView(stringResource(MR.strings.share_this_1_time_link), headerBottomPadding = 5.dp) {
LinkTextView(connLinkInvitation.simplexChatUri(short = showShortLink.value), true)
}
@@ -519,7 +521,7 @@ private fun InviteView(rhId: Long?, connLinkInvitation: CreatedConnLink, contact
val currentUser = remember { chatModel.currentUser }.value
if (currentUser != null) {
- SectionView(stringResource(MR.strings.new_chat_share_profile).uppercase(), headerBottomPadding = 5.dp) {
+ SectionView(stringResource(MR.strings.new_chat_share_profile), headerBottomPadding = 5.dp) {
SectionItemView(
padding = PaddingValues(
top = 0.dp,
@@ -643,14 +645,14 @@ private fun ConnectView(rhId: Long?, showQRCodeScanner: MutableState, p
)
}
- SectionView(stringResource(MR.strings.paste_the_link_you_received).uppercase(), headerBottomPadding = 5.dp) {
+ SectionView(stringResource(MR.strings.paste_the_link_you_received), headerBottomPadding = 5.dp) {
PasteLinkView(rhId, pastedLink, showQRCodeScanner, close)
}
if (appPlatform.isAndroid) {
Spacer(Modifier.height(10.dp))
- SectionView(stringResource(MR.strings.or_scan_qr_code).uppercase(), headerBottomPadding = 5.dp) {
+ SectionView(stringResource(MR.strings.or_scan_qr_code), headerBottomPadding = 5.dp) {
QRCodeScanner(showQRCodeScanner) { text ->
val linkVerified = verifyOnly(text)
if (!linkVerified) {
@@ -671,13 +673,14 @@ private fun PasteLinkView(rhId: Long?, pastedLink: MutableState, showQRC
val clipboard = LocalClipboardManager.current
SectionItemView({
val str = clipboard.getText()?.text ?: return@SectionItemView
- val link = strHasSingleSimplexLink(str.trim())
- if (link != null) {
- pastedLink.value = link.text
- showQRCodeScanner.value = false
- withBGApi { connect(rhId, link.text, close) { pastedLink.value = "" } }
- } else {
- AlertManager.shared.showAlertMsg(
+ when (val target = strConnectTarget(str.trim())) {
+ is ConnectTarget.Link -> {
+ pastedLink.value = target.text
+ showQRCodeScanner.value = false
+ withBGApi { connect(rhId, target.text, close) { pastedLink.value = "" } }
+ }
+ is ConnectTarget.Name -> showUnsupportedNameAlert(target.nameInfo)
+ null -> AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.invalid_contact_link),
text = generalGetString(MR.strings.the_text_you_pasted_is_not_a_link)
)
@@ -819,12 +822,32 @@ fun strIsSimplexLink(str: String): Boolean {
return parsedMd != null && parsedMd.size == 1 && parsedMd[0].format is Format.SimplexLink
}
-fun strHasSingleSimplexLink(str: String): FormattedText? {
- val parsedMd = parseToMarkdown(str) ?: return null
- val parsedLinks = parsedMd.filter { it.format?.isSimplexLink ?: false }
- if (parsedLinks.size != 1) return null
+sealed class ConnectTarget {
+ class Link(val text: String, val linkType: SimplexLinkType, val linkText: String) : ConnectTarget()
+ class Name(val nameInfo: SimplexNameInfo) : ConnectTarget()
+}
- return parsedLinks[0]
+fun strConnectTarget(str: String): ConnectTarget? {
+ val parsedMd = parseToMarkdown(str) ?: return null
+ val links = parsedMd.filter { it.format?.isSimplexLink ?: false }
+ if (links.size == 1) {
+ val fmt = links[0].format as Format.SimplexLink
+ return ConnectTarget.Link(links[0].text, fmt.linkType, fmt.simplexLinkText)
+ }
+ if (links.isEmpty()) {
+ val nameInfo = parsedMd.firstNotNullOfOrNull { (it.format as? Format.SimplexName)?.nameInfo }
+ if (nameInfo != null) return ConnectTarget.Name(nameInfo)
+ }
+ return null
+}
+
+fun showUnsupportedNameAlert(nameInfo: SimplexNameInfo) {
+ val (title, msg) = if (nameInfo.nameType == SimplexNameType.contact) {
+ generalGetString(MR.strings.unsupported_contact_name) to generalGetString(MR.strings.contact_name_requires_newer_app_version)
+ } else {
+ generalGetString(MR.strings.unsupported_channel_name) to generalGetString(MR.strings.channel_name_requires_newer_app_version)
+ }
+ AlertManager.shared.showAlertMsg(title, "$msg ${generalGetString(MR.strings.please_upgrade_the_app)}")
}
@Composable
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt
index 2fd77b46a1..d11e396388 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt
@@ -55,7 +55,7 @@ fun OnboardingConditionsView(chatModel: ChatModel) {
OnboardingConditionsDesktop(selectedOperatorIds)
} else {
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
- ModalView({}, showClose = false, showAppBar = false) {
+ ModalView({}, showClose = false, showAppBar = false, cardScreen = true) {
OnboardingShrinkingLayout(
modifier = Modifier.fillMaxSize().themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)
.systemBarsPadding()
@@ -133,7 +133,7 @@ fun OnboardingConditionsView(chatModel: ChatModel) {
@Composable
private fun OnboardingConditionsDesktop(selectedOperatorIds: MutableState>) {
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
- ModalView({}, showClose = false) {
+ ModalView({}, showClose = false, cardScreen = true) {
ColumnWithScrollBar(horizontalAlignment = Alignment.CenterHorizontally) {
Column(Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) {
Box(Modifier.align(Alignment.CenterHorizontally)) {
@@ -184,7 +184,7 @@ fun ModalData.ChooseServerOperators(
prepareChatBeforeFinishingOnboarding()
}
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
- ModalView(close, enableClose = selectedOperatorIds.value.isNotEmpty()) {
+ ModalView(close, enableClose = selectedOperatorIds.value.isNotEmpty(), cardScreen = true) {
ColumnWithScrollBar(
Modifier
.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer),
@@ -373,7 +373,7 @@ private fun ChooseServerOperatorsInfoView() {
SectionDividerSpaced()
- SectionView(title = stringResource(MR.strings.onboarding_network_about_operators).uppercase()) {
+ SectionView(title = stringResource(MR.strings.onboarding_network_about_operators)) {
chatModel.conditions.value.serverOperators.forEach { op ->
ServerOperatorRow(op)
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt
index e902b7947e..97dfcd34b9 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt
@@ -65,7 +65,7 @@ private fun LinkAMobileLayout(
Modifier.weight(0.3f),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
- SectionView(generalGetString(MR.strings.this_device_name).uppercase()) {
+ SectionView(generalGetString(MR.strings.this_device_name)) {
DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) }
SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile))
PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), checked = remember { ChatModel.controller.appPrefs.offerRemoteMulticast.state }.value) {
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt
index 8bb84060c2..81e1afd22c 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt
@@ -4,10 +4,10 @@ import SectionBottomSpacer
import SectionDividerSpaced
import SectionItemView
import SectionItemViewLongClickable
-import SectionSpacer
import SectionView
import TextIconSpaced
import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.background
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.*
import androidx.compose.runtime.*
@@ -29,8 +29,7 @@ import chat.simplex.common.model.ChatController.switchToLocalSession
import chat.simplex.common.model.ChatModel.connectedToRemote
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.platform.*
-import chat.simplex.common.ui.theme.DEFAULT_PADDING
-import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
+import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.QRCodeScanner
@@ -53,7 +52,7 @@ fun ConnectDesktopView(close: () -> Unit) {
showDisconnectDesktopAlert(close)
}
}
- ModalView(close = closeWithAlert) {
+ ModalView(close = closeWithAlert, cardScreen = true) {
ConnectDesktopLayout(
deviceName = deviceName.value!!,
close
@@ -128,7 +127,7 @@ private fun ConnectDesktopLayout(deviceName: String, close: () -> Unit) {
@Composable
private fun ConnectDesktop(deviceName: String, remoteCtrls: SnapshotStateList, sessionAddress: MutableState) {
AppBarTitle(stringResource(MR.strings.connect_to_desktop))
- SectionView(stringResource(MR.strings.this_device_name).uppercase()) {
+ SectionView(stringResource(MR.strings.this_device_name)) {
DevicesView(deviceName, remoteCtrls) {
if (it != "") {
setDeviceName(it)
@@ -139,7 +138,7 @@ private fun ConnectDesktop(deviceName: String, remoteCtrls: SnapshotStateList) {
AppBarTitle(stringResource(MR.strings.connecting_to_desktop))
- SectionView(stringResource(MR.strings.this_device_name).uppercase()) {
+ SectionView(stringResource(MR.strings.this_device_name)) {
DevicesView(deviceName, remoteCtrls) {
if (it != "") {
setDeviceName(it)
@@ -197,10 +196,10 @@ private fun SearchingDesktop(deviceName: String, remoteCtrls: SnapshotStateList<
}
}
SectionDividerSpaced()
- SectionView(stringResource(MR.strings.found_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
+ SectionView(stringResource(MR.strings.found_desktop), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(stringResource(MR.strings.waiting_for_desktop), fontStyle = FontStyle.Italic)
}
- SectionSpacer()
+ SectionDividerSpaced()
DisconnectButton(stringResource(MR.strings.scan_QR_code).replace('\n', ' '), MR.images.ic_qr_code, ::disconnectDesktop)
}
@@ -215,7 +214,7 @@ private fun FoundDesktop(
sessionAddress: MutableState,
) {
AppBarTitle(stringResource(MR.strings.found_desktop))
- SectionView(stringResource(MR.strings.this_device_name).uppercase()) {
+ SectionView(stringResource(MR.strings.this_device_name)) {
DevicesView(deviceName, remoteCtrls) {
if (it != "") {
setDeviceName(it)
@@ -224,7 +223,7 @@ private fun FoundDesktop(
}
}
SectionDividerSpaced()
- SectionView(stringResource(MR.strings.found_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
+ SectionView(stringResource(MR.strings.found_desktop), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
CtrlDeviceNameText(session, rc)
CtrlDeviceVersionText(session)
if (!compatible) {
@@ -232,7 +231,7 @@ private fun FoundDesktop(
}
}
- SectionSpacer()
+ SectionDividerSpaced()
if (compatible) {
SectionItemView({ withBGApi { confirmKnownDesktop(sessionAddress, rc) } }) {
@@ -256,19 +255,19 @@ private fun FoundDesktop(
@Composable
private fun VerifySession(session: RemoteCtrlSession, rc: RemoteCtrlInfo?, sessCode: String, remoteCtrls: SnapshotStateList) {
AppBarTitle(stringResource(MR.strings.verify_connection))
- SectionView(stringResource(MR.strings.connected_to_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
+ SectionView(stringResource(MR.strings.connected_to_desktop), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
CtrlDeviceNameText(session, rc)
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
CtrlDeviceVersionText(session)
}
- SectionSpacer()
+ SectionDividerSpaced()
- SectionView(stringResource(MR.strings.verify_code_with_desktop).uppercase()) {
+ SectionView(stringResource(MR.strings.verify_code_with_desktop)) {
SessionCodeText(sessCode)
}
- SectionSpacer()
+ SectionDividerSpaced()
SectionItemView({ verifyDesktopSessionCode(remoteCtrls, sessCode) }) {
Icon(painterResource(MR.images.ic_check), generalGetString(MR.strings.confirm_verb), tint = MaterialTheme.colors.secondary)
@@ -311,20 +310,20 @@ private fun CtrlDeviceVersionText(session: RemoteCtrlSession) {
@Composable
private fun ActiveSession(session: RemoteCtrlSession, rc: RemoteCtrlInfo, close: () -> Unit) {
AppBarTitle(stringResource(MR.strings.connected_to_desktop))
- SectionView(stringResource(MR.strings.connected_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
+ SectionView(stringResource(MR.strings.connected_desktop), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(rc.deviceViewName)
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
CtrlDeviceVersionText(session)
}
if (session.sessionCode != null) {
- SectionSpacer()
- SectionView(stringResource(MR.strings.session_code).uppercase()) {
+ SectionDividerSpaced()
+ SectionView(stringResource(MR.strings.session_code)) {
SessionCodeText(session.sessionCode!!)
}
}
- SectionSpacer()
+ SectionDividerSpaced()
SectionView {
DisconnectButton { disconnectDesktop(close) }
@@ -355,7 +354,7 @@ private fun DevicesView(deviceName: String, remoteCtrls: SnapshotStateList) {
- SectionView(stringResource(MR.strings.scan_qr_code_from_desktop).uppercase()) {
+ SectionView(stringResource(MR.strings.scan_qr_code_from_desktop)) {
QRCodeScanner { text ->
sessionAddress.value = text
connectDesktopAddress(sessionAddress, text)
@@ -366,7 +365,7 @@ private fun ScanDesktopAddressView(sessionAddress: MutableState) {
@Composable
private fun DesktopAddressView(sessionAddress: MutableState) {
val clipboard = LocalClipboardManager.current
- SectionView(stringResource(MR.strings.desktop_address).uppercase()) {
+ SectionView(stringResource(MR.strings.desktop_address)) {
if (sessionAddress.value.isEmpty()) {
SettingsActionItem(
painterResource(MR.images.ic_content_paste),
@@ -410,7 +409,7 @@ private fun DesktopAddressView(sessionAddress: MutableState) {
private fun LinkedDesktopsView(remoteCtrls: SnapshotStateList) {
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.linked_desktops))
- SectionView(stringResource(MR.strings.desktop_devices).uppercase()) {
+ SectionView(stringResource(MR.strings.desktop_devices)) {
remoteCtrls.forEach { rc ->
val showMenu = rememberSaveable { mutableStateOf(false) }
SectionItemViewLongClickable(click = {}, longClick = { showMenu.value = true }) {
@@ -427,7 +426,7 @@ private fun LinkedDesktopsView(remoteCtrls: SnapshotStateList) {
}
SectionDividerSpaced()
- SectionView(stringResource(MR.strings.linked_desktop_options).uppercase()) {
+ SectionView(stringResource(MR.strings.linked_desktop_options)) {
PreferenceToggle(stringResource(MR.strings.verify_connections), checked = remember { controller.appPrefs.confirmRemoteSessions.state }.value) {
controller.appPrefs.confirmRemoteSessions.set(it)
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt
index 1d01ab11ff..8caf038481 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt
@@ -92,7 +92,7 @@ fun ConnectMobileLayout(
) {
ColumnWithScrollBar {
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()) {
+ SectionView(generalGetString(MR.strings.this_device_name)) {
DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) }
SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile))
PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), checked = remember { controller.appPrefs.offerRemoteMulticast.state }.value) {
@@ -100,7 +100,7 @@ fun ConnectMobileLayout(
}
SectionDividerSpaced()
}
- SectionView(stringResource(MR.strings.devices).uppercase()) {
+ SectionView(stringResource(MR.strings.devices)) {
if (chatModel.localUserCreated.value == true) {
SettingsActionItemWithContent(text = stringResource(MR.strings.this_device), icon = painterResource(MR.images.ic_desktop), click = connectDesktop) {
if (connectedHost.value == null) {
@@ -215,7 +215,7 @@ private fun ConnectMobileViewLayout(
Spacer(Modifier.height(DEFAULT_PADDING))
}
if (deviceName != null || sessionCode != null) {
- SectionView(stringResource(MR.strings.connected_mobile).uppercase()) {
+ SectionView(stringResource(MR.strings.connected_mobile)) {
SelectionContainer {
Text(
deviceName ?: stringResource(MR.strings.new_mobile_device),
@@ -228,7 +228,7 @@ private fun ConnectMobileViewLayout(
}
if (sessionCode != null) {
- SectionView(stringResource(MR.strings.verify_code_on_mobile).uppercase()) {
+ SectionView(stringResource(MR.strings.verify_code_on_mobile)) {
SelectionContainer {
Text(
sessionCode.substring(0, 23),
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt
index e24c09afd0..1c9e602f1b 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt
@@ -1,11 +1,13 @@
package chat.simplex.common.views.usersettings
+import CARD_PADDING
+import LocalCardScreen
import SectionBottomSpacer
import SectionDividerSpaced
import SectionItemView
+import itemHPadding
import SectionItemViewSpaceBetween
import SectionItemViewWithoutMinPadding
-import SectionSpacer
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -58,9 +60,9 @@ expect fun AppearanceView(m: ChatModel)
object AppearanceScope {
@Composable
fun ProfileImageSection() {
- SectionView(stringResource(MR.strings.settings_section_title_profile_images).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
+ SectionView(stringResource(MR.strings.settings_section_title_profile_images), contentPadding = PaddingValues(horizontal = CARD_PADDING)) {
val image = remember { chatModel.currentUser }.value?.image
- Row(Modifier.padding(top = 10.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
+ Row(Modifier.padding(vertical = 10.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
val size = 60
Box(Modifier.offset(x = -(size / 12).dp)) {
if (!image.isNullOrEmpty()) {
@@ -91,9 +93,10 @@ object AppearanceScope {
@Composable
fun AppToolbarsSection() {
BoxWithConstraints {
- SectionView(stringResource(MR.strings.appearance_app_toolbars).uppercase()) {
+ SectionView(stringResource(MR.strings.appearance_app_toolbars)) {
SectionItemViewWithoutMinPadding {
Box(Modifier.weight(1f)) {
+ var fontScale by remember { mutableStateOf(1f) }
Text(
stringResource(MR.strings.appearance_in_app_bars_alpha),
Modifier.clickable(
@@ -102,7 +105,9 @@ object AppearanceScope {
) {
appPrefs.inAppBarsAlpha.set(appPrefs.inAppBarsDefaultAlpha)
},
- maxLines = 1
+ maxLines = 1,
+ fontSize = MaterialTheme.typography.body1.fontSize * fontScale,
+ onTextLayout = { if (it.hasVisualOverflow && fontScale > 0.5f) fontScale -= 0.05f }
)
}
Spacer(Modifier.padding(end = 10.dp))
@@ -175,7 +180,7 @@ object AppearanceScope {
@Composable
fun MessageShapeSection() {
BoxWithConstraints {
- SectionView(stringResource(MR.strings.settings_section_title_message_shape).uppercase()) {
+ SectionView(stringResource(MR.strings.settings_section_title_message_shape)) {
SectionItemViewWithoutMinPadding {
Text(stringResource(MR.strings.settings_message_shape_corner), Modifier.weight(1f))
Spacer(Modifier.width(10.dp))
@@ -205,8 +210,8 @@ object AppearanceScope {
@Composable
fun FontScaleSection() {
val localFontScale = remember { mutableStateOf(appPrefs.fontScale.get()) }
- SectionView(stringResource(MR.strings.appearance_font_size).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
- Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) {
+ SectionView(stringResource(MR.strings.appearance_font_size), contentPadding = PaddingValues(horizontal = CARD_PADDING)) {
+ Row(Modifier.padding(vertical = 10.dp), verticalAlignment = Alignment.CenterVertically) {
Box(Modifier.size(50.dp)
.background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22))
.clip(RoundedCornerShape(percent = 22))
@@ -409,26 +414,29 @@ object AppearanceScope {
}
if (appPlatform.isDesktop) {
- val itemWidth = (DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier - DEFAULT_PADDING * 2 - DEFAULT_PADDING_HALF * 3) / 4
- val itemHeight = (DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier - DEFAULT_PADDING * 2) / 4
+ val gridPadding = 12.dp
+ val cardPadding = if (LocalCardScreen.current) CARD_PADDING * 2 else 0.dp
+ val itemSize = (DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier - cardPadding - gridPadding * 5) / 4
val rows = ceil((PresetWallpaper.entries.size + 2) / 4f).roundToInt()
LazyVerticalGrid(
columns = GridCells.Fixed(4),
- Modifier.height(itemHeight * rows + DEFAULT_PADDING_HALF * (rows - 1) + DEFAULT_PADDING * 2),
- contentPadding = PaddingValues(DEFAULT_PADDING),
- verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF),
- horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF),
+ Modifier.height(itemSize * rows + gridPadding * (rows + 1)),
+ contentPadding = PaddingValues(gridPadding),
+ verticalArrangement = Arrangement.spacedBy(gridPadding),
+ horizontalArrangement = Arrangement.spacedBy(gridPadding),
) {
- gridContent(itemWidth, itemHeight)
+ gridContent(itemSize, itemSize)
}
} else {
- LazyHorizontalGrid(
+ val gridPadding = 14.dp
+ val itemSize = 81.dp
+ LazyHorizontalGrid(
rows = GridCells.Fixed(1),
- Modifier.height(80.dp + DEFAULT_PADDING * 2),
- contentPadding = PaddingValues(DEFAULT_PADDING),
- horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF),
+ Modifier.height(itemSize + gridPadding * 2),
+ contentPadding = PaddingValues(gridPadding),
+ horizontalArrangement = Arrangement.spacedBy(gridPadding),
) {
- gridContent(80.dp, 80.dp)
+ gridContent(itemSize, itemSize)
}
}
}
@@ -521,9 +529,7 @@ object AppearanceScope {
}
SectionView(stringResource(MR.strings.settings_section_title_themes)) {
- Spacer(Modifier.height(DEFAULT_PADDING_HALF))
ThemeDestinationPicker(themeUserDestination)
- Spacer(Modifier.height(DEFAULT_PADDING_HALF))
val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? ->
if (to != null) onImport(to)
@@ -555,7 +561,7 @@ object AppearanceScope {
color = if (chatModel.remoteHostId != null && themeUserDestination.value != null) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
)
}
- SectionSpacer()
+ SectionDividerSpaced()
}
val state: State = remember(appPrefs.currentTheme.get()) {
@@ -584,23 +590,23 @@ object AppearanceScope {
}
saveThemeToDatabase(null)
}
- }
- SectionItemView(click = {
- val user = themeUserDestination.value
- if (user == null) {
- ModalManager.start.showModal {
- val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? ->
- if (to != null) onImport(to)
+ SectionItemView(click = {
+ val user = themeUserDestination.value
+ if (user == null) {
+ ModalManager.start.showModal(cardScreen = true) {
+ val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? ->
+ if (to != null) onImport(to)
+ }
+ CustomizeThemeView { onChooseType(it, importWallpaperLauncher) }
+ }
+ } else {
+ ModalManager.start.showModalCloseable(cardScreen = true) { close ->
+ UserWallpaperEditorModal(chatModel.remoteHostId(), user.first, close)
}
- CustomizeThemeView { onChooseType(it, importWallpaperLauncher) }
- }
- } else {
- ModalManager.start.showModalCloseable { close ->
- UserWallpaperEditorModal(chatModel.remoteHostId(), user.first, close)
}
+ }) {
+ Text(stringResource(MR.strings.customize_theme_title))
}
- }) {
- Text(stringResource(MR.strings.customize_theme_title))
}
}
@@ -626,68 +632,70 @@ object AppearanceScope {
)
}
- WallpaperPresetSelector(
- selectedWallpaper = wallpaperType,
- baseTheme = currentTheme.base,
- currentColors = { type ->
- ThemeManager.currentColors(type, null, null, appPrefs.themeOverrides.get())
- },
- onChooseType = onChooseType
- )
-
- val type = MaterialTheme.wallpaper.type
- if (type is WallpaperType.Image) {
- SectionItemView(disabled = chatModel.remoteHostId != null, click = {
- val defaultActiveTheme = ThemeManager.defaultActiveTheme(appPrefs.themeOverrides.get())
- ThemeManager.saveAndApplyWallpaper(baseTheme, null)
- ThemeManager.removeTheme(defaultActiveTheme?.themeId)
- removeWallpaperFile(type.filename)
- saveThemeToDatabase(null)
- }) {
- Text(
- stringResource(MR.strings.theme_remove_image),
- color = if (chatModel.remoteHostId == null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
- )
- }
- SectionSpacer()
- }
-
- SectionView(stringResource(MR.strings.settings_section_title_chat_colors).uppercase()) {
- WallpaperSetupView(
- wallpaperType,
- baseTheme,
- MaterialTheme.wallpaper,
- MaterialTheme.appColors.sentMessage,
- MaterialTheme.appColors.sentQuote,
- MaterialTheme.appColors.receivedMessage,
- MaterialTheme.appColors.receivedQuote,
- editColor = { name ->
- editColor(name)
- },
- onTypeChange = { type ->
- ThemeManager.saveAndApplyWallpaper(baseTheme, type)
- saveThemeToDatabase(null)
+ SectionView {
+ WallpaperPresetSelector(
+ selectedWallpaper = wallpaperType,
+ baseTheme = currentTheme.base,
+ currentColors = { type ->
+ ThemeManager.currentColors(type, null, null, appPrefs.themeOverrides.get())
},
+ onChooseType = onChooseType
)
+ val type = MaterialTheme.wallpaper.type
+ if (type is WallpaperType.Image) {
+ SectionItemView(disabled = chatModel.remoteHostId != null, click = {
+ val defaultActiveTheme = ThemeManager.defaultActiveTheme(appPrefs.themeOverrides.get())
+ ThemeManager.saveAndApplyWallpaper(baseTheme, null)
+ ThemeManager.removeTheme(defaultActiveTheme?.themeId)
+ removeWallpaperFile(type.filename)
+ saveThemeToDatabase(null)
+ }) {
+ Text(
+ stringResource(MR.strings.theme_remove_image),
+ color = if (chatModel.remoteHostId == null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
+ )
+ }
+ }
}
SectionDividerSpaced()
+ WallpaperSetupView(
+ wallpaperType,
+ baseTheme,
+ MaterialTheme.wallpaper,
+ MaterialTheme.appColors.sentMessage,
+ MaterialTheme.appColors.sentQuote,
+ MaterialTheme.appColors.receivedMessage,
+ MaterialTheme.appColors.receivedQuote,
+ editColor = { name ->
+ editColor(name)
+ },
+ onTypeChange = { type ->
+ ThemeManager.saveAndApplyWallpaper(baseTheme, type)
+ saveThemeToDatabase(null)
+ },
+ firstSectionTitle = stringResource(MR.strings.settings_section_title_chat_colors),
+ )
+ SectionDividerSpaced()
+
CustomizeThemeColorsSection(currentTheme) { name ->
editColor(name)
}
- SectionDividerSpaced(maxBottomPadding = false)
+ SectionDividerSpaced()
val currentOverrides = remember(currentTheme) { ThemeManager.defaultActiveTheme(appPrefs.themeOverrides.get()) }
val canResetColors = currentTheme.base.hasChangedAnyColor(currentOverrides)
if (canResetColors) {
- SectionItemView({
- ThemeManager.resetAllThemeColors()
- saveThemeToDatabase(null)
- }) {
- Text(generalGetString(MR.strings.reset_color), color = colors.primary)
+ SectionView {
+ SectionItemView({
+ ThemeManager.resetAllThemeColors()
+ saveThemeToDatabase(null)
+ }) {
+ Text(generalGetString(MR.strings.reset_color), color = colors.primary)
+ }
}
- SectionSpacer()
+ SectionDividerSpaced()
}
SectionView {
@@ -1007,7 +1015,7 @@ object AppearanceScope {
SimpleXThemeOverride(currentColors()) {
ChatThemePreview(theme, wallpaperImage, wallpaperType, previewBackgroundColor, previewTintColor)
}
- SectionSpacer()
+ SectionDividerSpaced()
}
var currentColor by remember { mutableStateOf(initialColor) }
@@ -1084,7 +1092,7 @@ object AppearanceScope {
}) {
Text(generalGetString(MR.strings.reset_single_color), color = colors.primary)
}
- SectionSpacer()
+ SectionDividerSpaced()
}
}
@@ -1188,75 +1196,82 @@ fun WallpaperSetupView(
initialReceivedQuoteColor: Color,
editColor: (ThemeColor) -> Unit,
onTypeChange: (WallpaperType?) -> Unit,
+ firstSectionTitle: String? = null,
) {
- if (wallpaperType is WallpaperType.Image) {
- val state = remember(wallpaperType.scaleType, initialWallpaper?.type) { mutableStateOf(wallpaperType.scaleType ?: (initialWallpaper?.type as? WallpaperType.Image)?.scaleType ?: WallpaperScaleType.FILL) }
- val values = remember {
- WallpaperScaleType.entries.map { it to generalGetString(it.text) }
- }
- ExposedDropDownSettingRow(
- stringResource(MR.strings.wallpaper_scale),
- values,
- state,
- onSelected = { scaleType ->
- onTypeChange(wallpaperType.copy(scaleType = scaleType))
- }
- )
- }
+ val hasWallpaperSettings = wallpaperType is WallpaperType.Preset || wallpaperType is WallpaperType.Image
- if (wallpaperType is WallpaperType.Preset || (wallpaperType is WallpaperType.Image && wallpaperType.scaleType == WallpaperScaleType.REPEAT)) {
- val state = remember(wallpaperType, initialWallpaper?.type?.scale) { mutableStateOf(wallpaperType.scale ?: initialWallpaper?.type?.scale ?: 1f) }
- Row(Modifier.padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) {
- Text("${state.value}".substring(0, min("${state.value}".length, 4)), Modifier.width(50.dp))
- Slider(
- state.value,
- valueRange = 0.5f..2f,
- onValueChange = {
- if (wallpaperType is WallpaperType.Preset) {
- onTypeChange(wallpaperType.copy(scale = it))
- } else if (wallpaperType is WallpaperType.Image) {
- onTypeChange(wallpaperType.copy(scale = it))
- }
+ if (hasWallpaperSettings) {
+ SectionView(firstSectionTitle) {
+ if (wallpaperType is WallpaperType.Image) {
+ val state = remember(wallpaperType.scaleType, initialWallpaper?.type) { mutableStateOf(wallpaperType.scaleType ?: (initialWallpaper?.type as? WallpaperType.Image)?.scaleType ?: WallpaperScaleType.FILL) }
+ val values = remember {
+ WallpaperScaleType.entries.map { it to generalGetString(it.text) }
}
- )
+ ExposedDropDownSettingRow(
+ stringResource(MR.strings.wallpaper_scale),
+ values,
+ state,
+ onSelected = { scaleType ->
+ onTypeChange(wallpaperType.copy(scaleType = scaleType))
+ }
+ )
+ }
+
+ if (wallpaperType is WallpaperType.Preset || (wallpaperType is WallpaperType.Image && wallpaperType.scaleType == WallpaperScaleType.REPEAT)) {
+ val state = remember(wallpaperType, initialWallpaper?.type?.scale) { mutableStateOf(wallpaperType.scale ?: initialWallpaper?.type?.scale ?: 1f) }
+ Row(Modifier.padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) {
+ Text("${state.value}".substring(0, min("${state.value}".length, 4)), Modifier.width(50.dp))
+ Slider(
+ state.value,
+ valueRange = 0.5f..2f,
+ onValueChange = {
+ if (wallpaperType is WallpaperType.Preset) {
+ onTypeChange(wallpaperType.copy(scale = it))
+ } else if (wallpaperType is WallpaperType.Image) {
+ onTypeChange(wallpaperType.copy(scale = it))
+ }
+ }
+ )
+ }
+ }
+
+ val wallpaperBackgroundColor = initialWallpaper?.background ?: wallpaperType.defaultBackgroundColor(theme, MaterialTheme.colors.background)
+ SectionItemViewSpaceBetween({ editColor(ThemeColor.WALLPAPER_BACKGROUND) }) {
+ val title = generalGetString(MR.strings.color_wallpaper_background)
+ Text(title)
+ Icon(painterResource(MR.images.ic_circle_filled), title, tint = wallpaperBackgroundColor)
+ }
+ val wallpaperTintColor = initialWallpaper?.tint ?: wallpaperType.defaultTintColor(theme)
+ SectionItemViewSpaceBetween({ editColor(ThemeColor.WALLPAPER_TINT) }) {
+ val title = generalGetString(MR.strings.color_wallpaper_tint)
+ Text(title)
+ Icon(painterResource(MR.images.ic_circle_filled), title, tint = wallpaperTintColor)
+ }
}
+ SectionDividerSpaced()
}
- if (wallpaperType is WallpaperType.Preset || wallpaperType is WallpaperType.Image) {
- val wallpaperBackgroundColor = initialWallpaper?.background ?: wallpaperType.defaultBackgroundColor(theme, MaterialTheme.colors.background)
- SectionItemViewSpaceBetween({ editColor(ThemeColor.WALLPAPER_BACKGROUND) }) {
- val title = generalGetString(MR.strings.color_wallpaper_background)
+ SectionView(if (!hasWallpaperSettings) firstSectionTitle else null) {
+ SectionItemViewSpaceBetween({ editColor(ThemeColor.SENT_MESSAGE) }) {
+ val title = generalGetString(MR.strings.color_sent_message)
Text(title)
- Icon(painterResource(MR.images.ic_circle_filled), title, tint = wallpaperBackgroundColor)
+ Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialSentColor)
}
- val wallpaperTintColor = initialWallpaper?.tint ?: wallpaperType.defaultTintColor(theme)
- SectionItemViewSpaceBetween({ editColor(ThemeColor.WALLPAPER_TINT) }) {
- val title = generalGetString(MR.strings.color_wallpaper_tint)
+ SectionItemViewSpaceBetween({ editColor(ThemeColor.SENT_QUOTE) }) {
+ val title = generalGetString(MR.strings.color_sent_quote)
Text(title)
- Icon(painterResource(MR.images.ic_circle_filled), title, tint = wallpaperTintColor)
+ Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialSentQuoteColor)
+ }
+ SectionItemViewSpaceBetween({ editColor(ThemeColor.RECEIVED_MESSAGE) }) {
+ val title = generalGetString(MR.strings.color_received_message)
+ Text(title)
+ Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialReceivedColor)
+ }
+ SectionItemViewSpaceBetween({ editColor(ThemeColor.RECEIVED_QUOTE) }) {
+ val title = generalGetString(MR.strings.color_received_quote)
+ Text(title)
+ Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialReceivedQuoteColor)
}
- SectionSpacer()
- }
-
- SectionItemViewSpaceBetween({ editColor(ThemeColor.SENT_MESSAGE) }) {
- val title = generalGetString(MR.strings.color_sent_message)
- Text(title)
- Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialSentColor)
- }
- SectionItemViewSpaceBetween({ editColor(ThemeColor.SENT_QUOTE) }) {
- val title = generalGetString(MR.strings.color_sent_quote)
- Text(title)
- Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialSentQuoteColor)
- }
- SectionItemViewSpaceBetween({ editColor(ThemeColor.RECEIVED_MESSAGE) }) {
- val title = generalGetString(MR.strings.color_received_message)
- Text(title)
- Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialReceivedColor)
- }
- SectionItemViewSpaceBetween({ editColor(ThemeColor.RECEIVED_QUOTE) }) {
- val title = generalGetString(MR.strings.color_received_quote)
- Text(title)
- Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialReceivedQuoteColor)
}
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt
index dcb71a552d..2c729149d0 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt
@@ -4,9 +4,12 @@ import SectionBottomSpacer
import SectionDividerSpaced
import SectionTextFooter
import SectionView
+import androidx.compose.foundation.background
import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import chat.simplex.common.model.ChatController.appPrefs
+import chat.simplex.common.ui.theme.*
import chat.simplex.common.platform.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@@ -29,14 +32,14 @@ fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) ->
ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.start.showModalCloseable { TerminalView(false) } } }
ResetHintsItem(unchangedHints)
SettingsPreferenceItem(painterResource(MR.images.ic_code), stringResource(MR.strings.show_developer_options), developerTools)
- SectionTextFooter(
- generalGetString(if (devTools.value) MR.strings.show_dev_options else MR.strings.hide_dev_options) + " " +
- generalGetString(MR.strings.developer_options)
- )
}
+ SectionTextFooter(
+ generalGetString(if (devTools.value) MR.strings.show_dev_options else MR.strings.hide_dev_options) + " " +
+ generalGetString(MR.strings.developer_options)
+ )
if (devTools.value) {
- SectionDividerSpaced(maxTopPadding = true)
- SectionView(stringResource(MR.strings.developer_options_section).uppercase()) {
+ SectionDividerSpaced()
+ SectionView(stringResource(MR.strings.developer_options_section)) {
SettingsActionItemWithContent(painterResource(MR.images.ic_breaking_news), stringResource(MR.strings.debug_logs)) {
DefaultSwitch(
checked = remember { appPrefs.logLevel.state }.value <= LogLevel.DEBUG,
@@ -59,15 +62,15 @@ fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) ->
SettingsPreferenceItem(painterResource(MR.images.ic_avg_pace), stringResource(MR.strings.show_slow_api_calls), appPreferences.showSlowApiCalls)
}
}
- SectionDividerSpaced(maxTopPadding = true)
- SectionView(stringResource(MR.strings.deprecated_options_section).uppercase()) {
+ SectionDividerSpaced()
+ SectionView(stringResource(MR.strings.deprecated_options_section)) {
val simplexLinkMode = chatModel.controller.appPrefs.simplexLinkMode
SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = {
simplexLinkMode.set(it)
chatModel.simplexLinkMode.value = it
})
- SectionBottomSpacer()
}
+ SectionBottomSpacer()
}
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt
index 55bd796a3b..4a3806ab89 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt
@@ -68,7 +68,7 @@ private fun HiddenProfileLayout(
val passwordValid by remember { derivedStateOf { hidePassword.value == hidePassword.value.trim() } }
val confirmValid by remember { derivedStateOf { confirmHidePassword.value == "" || hidePassword.value == confirmHidePassword.value } }
val saveDisabled by remember { derivedStateOf { hidePassword.value == "" || !passwordValid || confirmHidePassword.value == "" || !confirmValid } }
- SectionView(stringResource(MR.strings.hidden_profile_password).uppercase()) {
+ SectionView(stringResource(MR.strings.hidden_profile_password)) {
SectionItemViewWithoutMinPadding {
PassphraseField(hidePassword, generalGetString(MR.strings.password_to_show), isValid = { passwordValid }, showStrength = true)
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt
index 2fc427cd2e..91324bb39a 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt
@@ -4,8 +4,11 @@ import SectionBottomSpacer
import SectionTextFooter
import SectionView
import SectionViewSelectable
+import androidx.compose.foundation.background
import androidx.compose.material.*
import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import chat.simplex.common.ui.theme.*
import androidx.compose.ui.text.AnnotatedString
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.capitalize
@@ -21,43 +24,28 @@ import kotlin.collections.ArrayList
fun NotificationsSettingsView(
chatModel: ChatModel,
) {
- val onNotificationPreviewModeSelected = { mode: NotificationPreviewMode ->
- chatModel.controller.appPrefs.notificationPreviewMode.set(mode.name)
- chatModel.notificationPreviewMode.value = mode
- }
-
NotificationsSettingsLayout(
notificationsMode = remember { chatModel.controller.appPrefs.notificationsMode.state },
- notificationPreviewMode = chatModel.notificationPreviewMode,
- showPage = { page ->
+ showNotificationsMode = {
ModalManager.start.showModalCloseable(true) {
- when (page) {
- CurrentPage.NOTIFICATIONS_MODE -> NotificationsModeView(chatModel.controller.appPrefs.notificationsMode.state) { changeNotificationsMode(it, chatModel) }
- CurrentPage.NOTIFICATION_PREVIEW_MODE -> NotificationPreviewView(chatModel.notificationPreviewMode, onNotificationPreviewModeSelected)
- }
+ NotificationsModeView(chatModel.controller.appPrefs.notificationsMode.state) { changeNotificationsMode(it, chatModel) }
}
},
)
}
-enum class CurrentPage {
- NOTIFICATIONS_MODE, NOTIFICATION_PREVIEW_MODE
-}
-
@Composable
fun NotificationsSettingsLayout(
notificationsMode: State,
- notificationPreviewMode: State,
- showPage: (CurrentPage) -> Unit,
+ showNotificationsMode: () -> Unit,
) {
val modes = remember { notificationModes() }
- val previewModes = remember { notificationPreviewModes() }
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.notifications))
SectionView(null) {
if (appPlatform == AppPlatform.ANDROID) {
- SettingsActionItemWithContent(null, stringResource(MR.strings.settings_notifications_mode_title), { showPage(CurrentPage.NOTIFICATIONS_MODE) }) {
+ SettingsActionItemWithContent(null, stringResource(MR.strings.settings_notifications_mode_title), showNotificationsMode) {
Text(
modes.firstOrNull { it.value == notificationsMode.value }?.title ?: "",
maxLines = 1,
@@ -66,17 +54,9 @@ fun NotificationsSettingsLayout(
)
}
}
- SettingsActionItemWithContent(null, stringResource(MR.strings.settings_notification_preview_mode_title), { showPage(CurrentPage.NOTIFICATION_PREVIEW_MODE) }) {
- Text(
- previewModes.firstOrNull { it.value == notificationPreviewMode.value }?.title ?: "",
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- color = MaterialTheme.colors.secondary
- )
- }
- if (platform.androidIsXiaomiDevice() && (notificationsMode.value == NotificationsMode.PERIODIC || notificationsMode.value == NotificationsMode.SERVICE)) {
- SectionTextFooter(annotatedStringResource(MR.strings.xiaomi_ignore_battery_optimization))
- }
+ }
+ if (platform.androidIsXiaomiDevice() && (notificationsMode.value == NotificationsMode.PERIODIC || notificationsMode.value == NotificationsMode.SERVICE)) {
+ SectionTextFooter(annotatedStringResource(MR.strings.xiaomi_ignore_battery_optimization))
}
SectionBottomSpacer()
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt
index fe9137ee35..63f3491d80 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt
@@ -1,13 +1,15 @@
package chat.simplex.common.views.usersettings
import SectionBottomSpacer
-import SectionDividerSpaced
import SectionItemView
+import SectionDividerSpaced
import SectionTextFooter
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
+import androidx.compose.ui.Modifier
+import chat.simplex.common.ui.theme.*
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@@ -47,6 +49,7 @@ fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) {
if (preferences == currentPreferences) close()
else showUnsavedChangesAlert({ savePrefs(close) }, close)
},
+ cardScreen = true,
) {
PreferencesLayout(
preferences,
@@ -81,27 +84,27 @@ private fun PreferencesLayout(
onTTLUpdated = onTTLUpdated
)
- SectionDividerSpaced(true, maxBottomPadding = false)
+ SectionDividerSpaced()
val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.allow) }
FeatureSection(ChatFeature.FullDelete, allowFullDeletion) {
applyPrefs(preferences.copy(fullDelete = SimpleChatPreference(allow = it)))
}
- SectionDividerSpaced(true, maxBottomPadding = false)
+ SectionDividerSpaced()
val allowReactions = remember(preferences) { mutableStateOf(preferences.reactions.allow) }
FeatureSection(ChatFeature.Reactions, allowReactions) {
applyPrefs(preferences.copy(reactions = SimpleChatPreference(allow = it)))
}
- SectionDividerSpaced(true, maxBottomPadding = false)
+ SectionDividerSpaced()
val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.allow) }
FeatureSection(ChatFeature.Voice, allowVoice) {
applyPrefs(preferences.copy(voice = SimpleChatPreference(allow = it)))
}
- SectionDividerSpaced(true, maxBottomPadding = false)
+ SectionDividerSpaced()
val allowCalls = remember(preferences) { mutableStateOf(preferences.calls.allow) }
FeatureSection(ChatFeature.Calls, allowCalls) {
applyPrefs(preferences.copy(calls = SimpleChatPreference(allow = it)))
}
- SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
+ SectionDividerSpaced()
ResetSaveButtons(
reset = reset,
save = savePrefs,
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt
index 2771b5ac62..cf34fd5a44 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt
@@ -1,10 +1,11 @@
package chat.simplex.common.views.usersettings
import SectionBottomSpacer
-import SectionDividerSpaced
import SectionItemView
+import SectionDividerSpaced
import SectionTextFooter
import SectionView
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
@@ -14,6 +15,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
@@ -72,6 +74,48 @@ fun PrivacySettingsView(
stringResource(MR.strings.sanitize_links_toggle),
chatModel.controller.appPrefs.privacySanitizeLinks
)
+ }
+ SectionDividerSpaced()
+
+ SectionView(stringResource(MR.strings.settings_section_title_files)) {
+ SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
+ BlurRadiusOptions(remember { appPrefs.privacyMediaBlurRadius.state }) {
+ appPrefs.privacyMediaBlurRadius.set(it)
+ }
+ }
+
+ val currentUser = chatModel.currentUser.value
+ if (currentUser != null && !chatModel.desktopNoUserNoRemote) {
+ SectionDividerSpaced()
+ ContacRequestsFromGroupsSection(
+ currentUser = currentUser,
+ setAutoAcceptGrpDirectInvs = { enable ->
+ withApi {
+ chatModel.controller.apiSetUserAutoAcceptMemberContacts(currentUser, enable)
+ chatModel.currentUser.value = currentUser.copy(autoAcceptMemberContacts = enable)
+ }
+ }
+ )
+ }
+
+ SectionDividerSpaced()
+ SectionView {
+ SettingsActionItem(
+ painterResource(MR.images.ic_more_horiz),
+ stringResource(MR.strings.more_privacy),
+ showSettingsModal { MorePrivacyView(it) }
+ )
+ }
+ SectionBottomSpacer()
+ }
+}
+
+@Composable
+fun MorePrivacyView(chatModel: ChatModel) {
+ ColumnWithScrollBar {
+ AppBarTitle(stringResource(MR.strings.more_privacy))
+
+ SectionView(stringResource(MR.strings.settings_section_title_chats)) {
SettingsPreferenceItem(
painterResource(MR.images.ic_chat_bubble),
stringResource(MR.strings.privacy_show_last_messages),
@@ -97,10 +141,6 @@ fun PrivacySettingsView(
SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable ->
withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) }
})
- SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
- BlurRadiusOptions(remember { appPrefs.privacyMediaBlurRadius.state }) {
- appPrefs.privacyMediaBlurRadius.set(it)
- }
SettingsPreferenceItem(painterResource(MR.images.ic_security), stringResource(MR.strings.protect_ip_address), chatModel.controller.appPrefs.privacyAskToApproveRelays)
}
SectionTextFooter(
@@ -110,9 +150,34 @@ fun PrivacySettingsView(
stringResource(MR.strings.without_tor_or_vpn_ip_address_will_be_visible_to_file_servers)
}
)
+ SectionDividerSpaced()
+
+ SectionView(stringResource(MR.strings.notifications)) {
+ val previewModes = remember { notificationPreviewModes() }
+ val notificationPreviewMode = remember { chatModel.notificationPreviewMode }
+ SettingsActionItemWithContent(
+ painterResource(MR.images.ic_visibility_off),
+ stringResource(MR.strings.settings_notification_preview_mode_title),
+ click = {
+ ModalManager.start.showModalCloseable(true) {
+ NotificationPreviewView(notificationPreviewMode) { mode ->
+ chatModel.controller.appPrefs.notificationPreviewMode.set(mode.name)
+ chatModel.notificationPreviewMode.value = mode
+ }
+ }
+ }
+ ) {
+ Text(
+ previewModes.firstOrNull { it.value == notificationPreviewMode.value }?.title ?: "",
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ color = MaterialTheme.colors.secondary
+ )
+ }
+ }
val currentUser = chatModel.currentUser.value
- if (currentUser != null) {
+ if (currentUser != null && !chatModel.desktopNoUserNoRemote) {
fun setSendReceiptsContacts(enable: Boolean, clearOverrides: Boolean) {
withLongRunningApi(slow = 60_000) {
val mrs = UserMsgReceiptSettings(enable, clearOverrides)
@@ -163,57 +228,40 @@ fun PrivacySettingsView(
}
}
- fun setAutoAcceptGrpDirectInvs(enable: Boolean) {
- withApi {
- chatModel.controller.apiSetUserAutoAcceptMemberContacts(currentUser, enable)
- chatModel.currentUser.value = currentUser.copy(autoAcceptMemberContacts = enable)
+ SectionDividerSpaced()
+ DeliveryReceiptsSection(
+ currentUser = currentUser,
+ setOrAskSendReceiptsContacts = { enable ->
+ val contactReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat ->
+ if (chat.chatInfo is ChatInfo.Direct) {
+ val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts
+ count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1)
+ } else {
+ count
+ }
+ }
+ if (contactReceiptsOverrides == 0) {
+ setSendReceiptsContacts(enable, clearOverrides = false)
+ } else {
+ showUserContactsReceiptsAlert(enable, contactReceiptsOverrides, ::setSendReceiptsContacts)
+ }
+ },
+ setOrAskSendReceiptsGroups = { enable ->
+ val groupReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat ->
+ if (chat.chatInfo is ChatInfo.Group) {
+ val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts
+ count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1)
+ } else {
+ count
+ }
+ }
+ if (groupReceiptsOverrides == 0) {
+ setSendReceiptsGroups(enable, clearOverrides = false)
+ } else {
+ showUserGroupsReceiptsAlert(enable, groupReceiptsOverrides, ::setSendReceiptsGroups)
+ }
}
- }
-
- if (!chatModel.desktopNoUserNoRemote) {
- SectionDividerSpaced(maxTopPadding = true)
- ContacRequestsFromGroupsSection(
- currentUser = currentUser,
- setAutoAcceptGrpDirectInvs = { enable ->
- setAutoAcceptGrpDirectInvs(enable)
- }
- )
-
- SectionDividerSpaced(maxTopPadding = true)
- DeliveryReceiptsSection(
- currentUser = currentUser,
- setOrAskSendReceiptsContacts = { enable ->
- val contactReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat ->
- if (chat.chatInfo is ChatInfo.Direct) {
- val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts
- count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1)
- } else {
- count
- }
- }
- if (contactReceiptsOverrides == 0) {
- setSendReceiptsContacts(enable, clearOverrides = false)
- } else {
- showUserContactsReceiptsAlert(enable, contactReceiptsOverrides, ::setSendReceiptsContacts)
- }
- },
- setOrAskSendReceiptsGroups = { enable ->
- val groupReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat ->
- if (chat.chatInfo is ChatInfo.Group) {
- val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts
- count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1)
- } else {
- count
- }
- }
- if (groupReceiptsOverrides == 0) {
- setSendReceiptsGroups(enable, clearOverrides = false)
- } else {
- showUserGroupsReceiptsAlert(enable, groupReceiptsOverrides, ::setSendReceiptsGroups)
- }
- }
- )
- }
+ )
}
SectionBottomSpacer()
}
@@ -619,7 +667,7 @@ fun SimplexLockView(
}
if (performLA.value && laMode.value == LAMode.PASSCODE) {
SectionDividerSpaced()
- SectionView(stringResource(MR.strings.self_destruct_passcode).uppercase()) {
+ SectionView(stringResource(MR.strings.self_destruct_passcode)) {
val openInfo = {
ModalManager.start.showModal {
SelfDestructInfoView()
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt
index a02d67265d..c8e040c592 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt
@@ -1,8 +1,9 @@
package chat.simplex.common.views.usersettings
import SectionBottomSpacer
-import SectionDividerSpaced
+import itemHPadding
import SectionItemView
+import SectionDividerSpaced
import SectionView
import TextIconSpaced
import androidx.compose.desktop.ui.tooling.preview.Preview
@@ -36,22 +37,21 @@ import chat.simplex.res.MR
@Composable
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: () -> Unit) {
- val user = chatModel.currentUser.value
val stopped = chatModel.chatRunning.value == false
+ val showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit) = { modalView -> { ModalManager.start.showModal(settings = true, cardScreen = true) { modalView(chatModel) } } }
SettingsLayout(
stopped,
chatModel.chatDbEncrypted.value == true,
remember { chatModel.controller.appPrefs.storeDBPassphrase.state }.value,
- remember { chatModel.controller.appPrefs.notificationsMode.state },
- user?.displayName,
setPerformLA = setPerformLA,
showModal = { modalView -> { ModalManager.start.showModal { modalView(chatModel) } } },
- showSettingsModal = { modalView -> { ModalManager.start.showModal(true) { modalView(chatModel) } } },
+ showSettingsModal = showSettingsModal,
showSettingsModalWithSearch = { modalView ->
ModalManager.start.showCustomModal { close ->
val search = rememberSaveable { mutableStateOf("") }
ModalView(
{ close() },
+ cardScreen = true,
showSearch = true,
searchAlwaysVisible = true,
onSearchValueChanged = { search.value = it },
@@ -60,12 +60,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: (
},
showCustomModal = { modalView -> { ModalManager.start.showCustomModal { close -> modalView(chatModel, close) } } },
showVersion = {
- withBGApi {
- val info = chatModel.controller.apiGetVersion()
- if (info != null) {
- ModalManager.start.showModal { VersionInfoView(info) }
- }
- }
+ ModalManager.start.showModal(cardScreen = true) { VersionInfoView(showSettingsModal, ::doWithAuth) }
},
withAuth = ::doWithAuth,
)
@@ -82,8 +77,6 @@ fun SettingsLayout(
stopped: Boolean,
encrypted: Boolean,
passphraseSaved: Boolean,
- notificationsMode: State,
- userDisplayName: String?,
setPerformLA: (Boolean) -> Unit,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
@@ -96,30 +89,52 @@ fun SettingsLayout(
LaunchedEffect(Unit) {
hideKeyboard(view)
}
- val uriHandler = LocalUriHandler.current
+ val notificationsMode = remember { chatModel.controller.appPrefs.notificationsMode.state }
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.your_settings))
- SectionView(stringResource(MR.strings.settings_section_title_settings)) {
- SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped)
- SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showCustomModal { _, close -> NetworkAndServersView(close) }, disabled = stopped)
- SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped)
- SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped)
+ SectionView {
SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it) })
- }
- SectionDividerSpaced()
-
- SectionView(stringResource(MR.strings.settings_section_title_chat_database)) {
+ SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.your_privacy), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped)
+ SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.help_and_support), showSettingsModal { HelpAndSupportView(it, showModal, showCustomModal) })
DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView() }, stopped)
SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_from_device_to_another_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateFromDeviceView(close) } } }, disabled = stopped)
}
-
SectionDividerSpaced()
+ SectionView(stringResource(MR.strings.advanced_settings)) {
+ SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showCustomModal { _, close -> NetworkAndServersView(close) }, disabled = stopped)
+ if (appPlatform == AppPlatform.ANDROID) {
+ SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped)
+ }
+ SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped)
+ AppShutdownItem()
+ AppVersionItem(showVersion)
+ }
+ SectionBottomSpacer()
+ }
+}
+
+@Composable
+fun HelpAndSupportView(
+ chatModel: ChatModel,
+ showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
+ showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
+) {
+ val uriHandler = LocalUriHandler.current
+ val stopped = chatModel.chatRunning.value == false
+ val userDisplayName = chatModel.currentUser.value?.displayName ?: ""
+ ColumnWithScrollBar {
+ AppBarTitle(stringResource(MR.strings.help_and_support))
+
SectionView(stringResource(MR.strings.settings_section_title_help)) {
- SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName ?: "") }, disabled = stopped)
+ SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName) }, disabled = stopped)
SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close = close) }, disabled = stopped)
SettingsActionItem(painterResource(MR.images.ic_info), stringResource(MR.strings.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) })
+ }
+ SectionDividerSpaced()
+
+ SectionView(stringResource(MR.strings.settings_section_title_contact)) {
if (!chatModel.desktopNoUserNoRemote) {
SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped)
}
@@ -127,27 +142,29 @@ fun SettingsLayout(
}
SectionDividerSpaced()
- SectionView(stringResource(MR.strings.settings_section_title_support)) {
+ SectionView(stringResource(MR.strings.settings_section_title_support_project)) {
if (!BuildConfigCommon.ANDROID_BUNDLE) {
ContributeItem(uriHandler)
}
- RateAppItem(uriHandler)
+ if (appPlatform.isAndroid) {
+ RateAppItem(uriHandler)
+ }
StarOnGithubItem(uriHandler)
}
- SectionDividerSpaced()
-
- SettingsSectionApp(showSettingsModal, showVersion, withAuth)
SectionBottomSpacer()
}
}
@Composable
-expect fun SettingsSectionApp(
+expect fun AdvancedSettingsAppSection(
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
- showVersion: () -> Unit,
- withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
+ withAuth: (title: String, desc: String, block: () -> Unit) -> Unit,
)
+// Shutdown is only available on Android; on desktop the app is closed via the window.
+@Composable
+expect fun AppShutdownItem()
+
@Composable private fun DatabaseItem(encrypted: Boolean, saved: Boolean, openDatabaseView: () -> Unit, stopped: Boolean) {
SectionItemView(openDatabaseView) {
Row(
@@ -158,11 +175,11 @@ expect fun SettingsSectionApp(
Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) {
Icon(
painterResource(MR.images.ic_database),
- contentDescription = stringResource(MR.strings.database_passphrase_and_export),
+ contentDescription = stringResource(MR.strings.chat_data),
tint = if (encrypted && (appPlatform.isAndroid || !saved)) MaterialTheme.colors.secondary else WarningOrange,
)
TextIconSpaced(false)
- Text(stringResource(MR.strings.database_passphrase_and_export))
+ Text(stringResource(MR.strings.chat_data))
}
if (stopped) {
Icon(
@@ -206,7 +223,7 @@ fun ChatLockItem(
}
}
-@Composable private fun ContributeItem(uriHandler: UriHandler) {
+@Composable fun ContributeItem(uriHandler: UriHandler) {
SectionItemView({ uriHandler.openExternalLink("https://github.com/simplex-chat/simplex-chat#contribute") }) {
Icon(
painterResource(MR.images.ic_keyboard),
@@ -218,7 +235,7 @@ fun ChatLockItem(
}
}
-@Composable private fun RateAppItem(uriHandler: UriHandler) {
+@Composable fun RateAppItem(uriHandler: UriHandler) {
SectionItemView({
runCatching { uriHandler.openUriCatching("market://details?id=chat.simplex.app") }
.onFailure { uriHandler.openUriCatching("https://play.google.com/store/apps/details?id=chat.simplex.app") }
@@ -234,7 +251,7 @@ fun ChatLockItem(
}
}
-@Composable private fun StarOnGithubItem(uriHandler: UriHandler) {
+@Composable fun StarOnGithubItem(uriHandler: UriHandler) {
SectionItemView({ uriHandler.openExternalLink("https://github.com/simplex-chat/simplex-chat") }) {
Icon(
painter = painterResource(MR.images.ic_github),
@@ -309,12 +326,13 @@ fun AppVersionItem(showVersion: () -> Unit) {
Text(appVersionInfo.first + (if (appVersionInfo.second != null) " (" + appVersionInfo.second + ")" else ""))
}
-@Composable fun ProfilePreview(profileOf: NamedChat, size: Dp = 60.dp, iconColor: Color = MaterialTheme.colors.secondaryVariant, textColor: Color = MaterialTheme.colors.onBackground, stopped: Boolean = false) {
+@Composable fun ProfilePreview(profileOf: NamedChat, size: Dp = 60.dp, iconColor: Color = MaterialTheme.colors.secondaryVariant, textColor: Color = MaterialTheme.colors.onBackground, stopped: Boolean = false, badge: LocalBadge? = null) {
ProfileImage(size = size, image = profileOf.image, color = iconColor)
Spacer(Modifier.padding(horizontal = 8.dp))
Column(Modifier.height(size), verticalArrangement = Arrangement.Center) {
- Text(
+ NameWithBadge(
profileOf.displayName,
+ badge,
style = MaterialTheme.typography.caption,
fontWeight = FontWeight.Bold,
color = if (stopped) MaterialTheme.colors.secondary else textColor,
@@ -348,9 +366,9 @@ fun SettingsActionItemWithContent(icon: Painter?, text: String? = null, click: (
click,
extraPadding = extraPadding,
padding = if (extraPadding && icon != null)
- PaddingValues(start = DEFAULT_PADDING * 1.7f, end = DEFAULT_PADDING)
+ PaddingValues(start = DEFAULT_PADDING * 1.7f, end = itemHPadding)
else
- PaddingValues(horizontal = DEFAULT_PADDING),
+ PaddingValues(horizontal = itemHPadding),
disabled = disabled
) {
if (icon != null) {
@@ -484,8 +502,6 @@ fun PreviewSettingsLayout() {
stopped = false,
encrypted = false,
passphraseSaved = false,
- notificationsMode = remember { mutableStateOf(NotificationsMode.OFF) },
- userDisplayName = "Alice",
setPerformLA = { _ -> },
showModal = { {} },
showSettingsModal = { {} },
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt
index e5c731f3b2..c55eaf6c10 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt
@@ -8,6 +8,7 @@ import SectionView
import SectionViewWithButton
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.ui.layout.ContentScale
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -171,7 +172,7 @@ fun UserAddressView(
)
}
- ModalView(close = close) {
+ ModalView(close = close, cardScreen = true) {
showLayout()
}
@@ -301,16 +302,16 @@ private fun UserAddressLayout(
) {
if (userAddress == null) {
if (!onboarding) {
- SectionView(generalGetString(MR.strings.for_social_media).uppercase()) {
+ SectionView(generalGetString(MR.strings.for_social_media)) {
CreateAddressButton(createAddress)
}
SectionDividerSpaced()
- SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) {
+ SectionView(generalGetString(MR.strings.or_to_share_privately)) {
CreateOneTimeLinkButton()
}
- SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
+ SectionDividerSpaced()
SectionView {
LearnMoreButton(learnMore)
}
@@ -336,7 +337,7 @@ private fun UserAddressLayout(
val savedAddressSettingsState = remember { mutableStateOf(addressSettingsState.value) }
SectionViewWithButton(
- stringResource(MR.strings.for_social_media).uppercase(),
+ stringResource(MR.strings.for_social_media),
titleButton = if (userAddress.connLinkContact.connShortLink != null) {{ ToggleShortLinkButton(showShortLink) }} else null
) {
SimpleXCreatedLinkQRCode(userAddress.connLinkContact, short = showShortLink.value)
@@ -353,26 +354,25 @@ private fun UserAddressLayout(
// ShareViaEmailButton { sendEmail(userAddress) }
BusinessAddressToggle(addressSettingsState) { saveAddressSettings(addressSettingsState.value, savedAddressSettingsState) }
AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAddressSettings)
-
- if (addressSettingsState.value.businessAddress) {
- SectionTextFooter(stringResource(MR.strings.add_your_team_members_to_conversations))
- }
+ }
+ if (addressSettingsState.value.businessAddress) {
+ SectionTextFooter(stringResource(MR.strings.add_your_team_members_to_conversations))
}
- SectionDividerSpaced(maxTopPadding = addressSettingsState.value.businessAddress)
- SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) {
+ SectionDividerSpaced()
+ SectionView(generalGetString(MR.strings.or_to_share_privately)) {
CreateOneTimeLinkButton()
}
- SectionDividerSpaced(maxBottomPadding = false)
+ SectionDividerSpaced()
SectionView {
LearnMoreButton(learnMore)
}
- SectionDividerSpaced(maxBottomPadding = false)
+ SectionDividerSpaced()
SectionView {
DeleteAddressButton(deleteAddress)
- SectionTextFooter(stringResource(MR.strings.your_contacts_will_remain_connected))
}
+ SectionTextFooter(stringResource(MR.strings.your_contacts_will_remain_connected))
}
}
}
@@ -495,7 +495,7 @@ private fun ModalData.UserAddressSettings(
}
}
- ModalView(close = { onClose(close) }) {
+ ModalView(close = { onClose(close) }, cardScreen = true) {
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.address_settings), hostDevice(user?.remoteHostId))
Column(
@@ -512,10 +512,10 @@ private fun ModalData.UserAddressSettings(
}
SectionDividerSpaced()
- SectionView(stringResource(MR.strings.address_welcome_message).uppercase()) {
+ SectionView(stringResource(MR.strings.address_welcome_message)) {
AutoReplyEditor(addressSettingsState)
}
- SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
+ SectionDividerSpaced()
saveAddressSettingsButton(addressSettingsState.value == savedAddressSettingsState.value) {
saveAddressSettings(addressSettingsState.value, savedAddressSettingsState)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt
index d7ddb6b950..ac21fb6b23 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt
@@ -1,7 +1,6 @@
package chat.simplex.common.views.usersettings
import SectionBottomSpacer
-import SectionDivider
import SectionItemView
import SectionItemViewSpaceBetween
import SectionItemViewWithoutMinPadding
@@ -177,7 +176,7 @@ private fun UserProfilesLayout(
SectionView {
for (user in filteredUsers) {
UserView(user, visibleUsersCount, activateUser, removeUser, unhideUser, muteUser, unmuteUser, showHiddenProfile)
- SectionDivider()
+ Divider(Modifier.padding(horizontal = 8.dp))
}
if (searchTextOrPassword.value.trim().isEmpty()) {
SectionItemView(addUser, minHeight = 68.dp) {
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt
index 52addd146b..5070c3c0aa 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt
@@ -1,33 +1,55 @@
package chat.simplex.common.views.usersettings
+import SectionBottomSpacer
+import SectionDividerSpaced
+import SectionView
+import itemHPadding
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
+import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.common.BuildConfigCommon
+import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.CoreVersionInfo
import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.common.platform.appPlatform
-import chat.simplex.common.ui.theme.DEFAULT_PADDING
+import chat.simplex.common.platform.chatModel
+import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
import chat.simplex.common.views.helpers.AppBarTitle
import chat.simplex.res.MR
@Composable
-fun VersionInfoView(info: CoreVersionInfo) {
- ColumnWithScrollBar(
- Modifier.padding(horizontal = DEFAULT_PADDING),
- ) {
- AppBarTitle(stringResource(MR.strings.app_version_title), withPadding = false)
- if (appPlatform.isAndroid) {
- Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.ANDROID_VERSION_NAME))
- Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.ANDROID_VERSION_CODE))
- } else {
- Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.DESKTOP_VERSION_NAME))
- Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.DESKTOP_VERSION_CODE))
+fun VersionInfoView(
+ showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
+ withAuth: (title: String, desc: String, block: () -> Unit) -> Unit,
+) {
+ val versionInfo = remember { mutableStateOf(null) }
+ LaunchedEffect(Unit) {
+ versionInfo.value = chatModel.controller.apiGetVersion()
+ }
+ ColumnWithScrollBar {
+ AppBarTitle(stringResource(MR.strings.app_version_title))
+ SectionView {
+ Column(Modifier.padding(horizontal = itemHPadding, vertical = DEFAULT_PADDING_HALF)) {
+ if (appPlatform.isAndroid) {
+ Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.ANDROID_VERSION_NAME))
+ Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.ANDROID_VERSION_CODE))
+ } else {
+ Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.DESKTOP_VERSION_NAME))
+ Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.DESKTOP_VERSION_CODE))
+ }
+ versionInfo.value?.let { info ->
+ Text(String.format(stringResource(MR.strings.core_version), info.version))
+ val simplexmqCommit = if (info.simplexmqCommit.length >= 7) info.simplexmqCommit.substring(startIndex = 0, endIndex = 7) else info.simplexmqCommit
+ Text(String.format(stringResource(MR.strings.core_simplexmq_version), info.simplexmqVersion, simplexmqCommit))
+ }
+ }
}
- Text(String.format(stringResource(MR.strings.core_version), info.version))
- val simplexmqCommit = if (info.simplexmqCommit.length >= 7) info.simplexmqCommit.substring(startIndex = 0, endIndex = 7) else info.simplexmqCommit
- Text(String.format(stringResource(MR.strings.core_simplexmq_version), info.simplexmqVersion, simplexmqCommit))
+ SectionDividerSpaced()
+
+ AdvancedSettingsAppSection(showSettingsModal, withAuth)
+ SectionBottomSpacer()
}
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt
index 8c38070c98..42746006a3 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt
@@ -8,6 +8,7 @@ import SectionTextFooter
import SectionView
import SectionViewSelectableCards
import androidx.compose.desktop.ui.tooling.preview.Preview
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
@@ -158,6 +159,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (@Composable ModalData.() -
}, close)
}
},
+ cardScreen = true,
) {
AdvancedNetworkSettingsLayout(
currentRemoteHost = currentRemoteHost,
@@ -234,13 +236,13 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (@Composable ModalData.() -
SettingsPreferenceItem(painterResource(MR.images.ic_arrow_forward), stringResource(MR.strings.private_routing_show_message_status), chatModel.controller.appPrefs.showSentViaProxy)
}
SectionTextFooter(stringResource(MR.strings.private_routing_explanation))
- SectionDividerSpaced(maxTopPadding = true)
+ SectionDividerSpaced()
- SectionView(stringResource(MR.strings.network_session_mode_transport_isolation).uppercase()) {
+ SectionView(stringResource(MR.strings.network_session_mode_transport_isolation)) {
SessionModePicker(sessionMode, showModal, updateSessionMode)
}
SectionDividerSpaced()
- SectionView(stringResource(MR.strings.network_smp_web_port_section_title).uppercase()) {
+ SectionView(stringResource(MR.strings.network_smp_web_port_section_title)) {
ExposedDropDownSettingRow(
stringResource(MR.strings.network_smp_web_port_toggle),
SMPWebPortServers.entries.map { it to stringResource(it.text) },
@@ -251,9 +253,9 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (@Composable ModalData.() -
if (smpWebPortServers.value == SMPWebPortServers.Preset) stringResource(MR.strings.network_smp_web_port_preset_footer)
else String.format(stringResource(MR.strings.network_smp_web_port_footer), if (smpWebPortServers.value == SMPWebPortServers.All) "443" else "5223")
)
- SectionDividerSpaced(maxTopPadding = true)
+ SectionDividerSpaced()
- SectionView(stringResource(MR.strings.network_option_tcp_connection).uppercase()) {
+ SectionView(stringResource(MR.strings.network_option_tcp_connection)) {
SectionItemView {
TimeoutSettingRow(
stringResource(MR.strings.network_option_tcp_connection_timeout), networkTCPConnectTimeoutInteractive,
@@ -330,7 +332,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (@Composable ModalData.() -
}
}
- SectionDividerSpaced(maxBottomPadding = false)
+ SectionDividerSpaced()
SectionView {
SectionItemView(reset, disabled = resetDisabled) {
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ChatRelayView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ChatRelayView.kt
index 1c68e780dc..ab63067226 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ChatRelayView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ChatRelayView.kt
@@ -149,7 +149,8 @@ fun ChatRelayView(
text = generalGetString(MR.strings.check_relay_address)
)
}
- }
+ },
+ cardScreen = true,
) {
ChatRelayLayout(
relayToEdit,
@@ -182,7 +183,7 @@ private fun ChatRelayLayout(
@Composable
private fun PresetRelay(relay: MutableState, testing: MutableState) {
- SectionView(stringResource(MR.strings.preset_relay_address).uppercase()) {
+ SectionView(stringResource(MR.strings.preset_relay_address)) {
SelectionContainer {
Text(
relay.value.address,
@@ -192,7 +193,7 @@ private fun PresetRelay(relay: MutableState, testing: MutableStat
}
}
SectionDividerSpaced()
- SectionView(stringResource(MR.strings.preset_relay_name).uppercase()) {
+ SectionView(stringResource(MR.strings.preset_relay_name)) {
SectionItemView {
Text(relay.value.displayName)
}
@@ -291,7 +292,7 @@ private fun UseRelaySection(
testing: MutableState
) {
val scope = rememberCoroutineScope()
- SectionView(stringResource(MR.strings.use_relay).uppercase()) {
+ SectionView(stringResource(MR.strings.use_relay)) {
SectionItemViewSpaceBetween(
click = {
testing.value = true
@@ -377,7 +378,7 @@ fun ModalData.NewChatRelayView(
ModalView(close = {
addChatRelay(relayToEdit.value, userServers, serverErrors, serverWarnings, rhId, close)
- }) {
+ }, cardScreen = true) {
NewChatRelayLayout(relayToEdit)
}
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt
index a62a58cb10..892a252a9d 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt
@@ -9,6 +9,7 @@ import SectionTextFooter
import SectionView
import SectionViewSelectable
import TextIconSpaced
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.*
@@ -84,7 +85,7 @@ fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) {
onClose(close = { ModalManager.start.closeModals() })
}
}
- ModalView(close = { onClose(closeNetworkAndServers) }) {
+ ModalView(close = { onClose(closeNetworkAndServers) }, cardScreen = true) {
NetworkAndServersLayout(
currentRemoteHost = currentRemoteHost,
networkUseSocksProxy = networkUseSocksProxy,
@@ -210,7 +211,7 @@ fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) {
AppBarTitle(stringResource(MR.strings.network_and_servers))
// TODO: Review this and socks.
if (!chatModel.desktopNoUserNoRemote) {
- SectionView(generalGetString(MR.strings.network_preset_servers_title).uppercase()) {
+ SectionView(generalGetString(MR.strings.network_preset_servers_title)) {
userServers.value.forEachIndexed { index, srv ->
srv.operator?.let { ServerOperatorRow(index, it, currUserServers, userServers, serverErrors, serverWarnings, currentRemoteHost?.remoteHostId) }
}
@@ -262,14 +263,11 @@ fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) {
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, appPrefs.networkProxy, onionHosts, sessionMode = appPrefs.networkSessionMode.get(), false, it) } })
SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), { ModalManager.start.showCustomModal { AdvancedNetworkSettingsView(showModal, it) } })
- if (networkUseSocksProxy.value) {
- SectionTextFooter(annotatedStringResource(MR.strings.socks_proxy_setting_limitations))
- SectionDividerSpaced(maxTopPadding = true)
- } else {
- SectionDividerSpaced(maxBottomPadding = false)
- }
}
}
+ if (currentRemoteHost == null && networkUseSocksProxy.value) {
+ SectionTextFooter(annotatedStringResource(MR.strings.socks_proxy_setting_limitations))
+ }
val saveDisabled = !serversCanBeSaved(currUserServers.value, userServers.value, serverErrors.value)
SectionItemView(
@@ -303,7 +301,7 @@ fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) {
if (appPlatform.isAndroid) {
SectionDividerSpaced()
- SectionView(generalGetString(MR.strings.settings_section_title_network_connection).uppercase()) {
+ SectionView(generalGetString(MR.strings.settings_section_title_network_connection)) {
val info = remember { chatModel.networkInfo }.value
SettingsActionItemWithContent(icon = null, info.networkType.text) {
Icon(painterResource(MR.images.ic_circle_filled), stringResource(MR.strings.icon_descr_server_status_connected), tint = if (info.online) Color.Green else MaterialTheme.colors.error)
@@ -466,10 +464,11 @@ fun SocksProxySettings(
)
}
},
+ cardScreen = true,
) {
ColumnWithScrollBar {
AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings))
- SectionView(stringResource(MR.strings.network_socks_proxy).uppercase()) {
+ SectionView(stringResource(MR.strings.network_socks_proxy)) {
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
DefaultConfigurableTextField(
hostUnsaved,
@@ -495,9 +494,9 @@ fun SocksProxySettings(
SectionTextFooter(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported))
}
- SectionDividerSpaced(maxTopPadding = true)
+ SectionDividerSpaced()
- SectionView(stringResource(MR.strings.network_proxy_auth).uppercase()) {
+ SectionView(stringResource(MR.strings.network_proxy_auth)) {
PreferenceToggle(
stringResource(MR.strings.network_proxy_random_credentials),
checked = proxyAuthRandomUnsaved.value,
@@ -526,7 +525,7 @@ fun SocksProxySettings(
SectionTextFooter(proxyAuthFooter(usernameUnsaved.value.text, passwordUnsaved.value.text, proxyAuthModeUnsaved.value, sessionMode))
}
- SectionDividerSpaced(maxBottomPadding = false, maxTopPadding = true)
+ SectionDividerSpaced()
SectionView {
SectionItemView({
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt
index 9e11b9a932..f5bceabbf1 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt
@@ -10,6 +10,7 @@ import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
+import androidx.compose.foundation.background
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -181,7 +182,7 @@ fun OperatorViewLayout(
val duplicateHosts = findDuplicateHosts(serverErrors.value)
Column {
- SectionView(generalGetString(MR.strings.operator).uppercase()) {
+ SectionView(generalGetString(MR.strings.operator)) {
SectionItemView({ ModalManager.start.showModalCloseable { _ -> OperatorInfoView(operator) } }) {
Row(
Modifier.fillMaxWidth(),
@@ -238,7 +239,7 @@ fun OperatorViewLayout(
if (userServers.value[operatorIndex].chatRelays.any { !it.deleted }) {
val duplicateRelayAddresses = findDuplicateRelayAddresses(serverErrors.value)
SectionDividerSpaced()
- SectionView(generalGetString(MR.strings.chat_relays).uppercase()) {
+ SectionView(generalGetString(MR.strings.chat_relays)) {
userServers.value[operatorIndex].chatRelays.forEachIndexed { index, relay ->
if (!relay.deleted) {
ChatRelayViewLink(relay, duplicateRelayAddresses) {
@@ -252,7 +253,7 @@ fun OperatorViewLayout(
if (userServers.value[operatorIndex].smpServers.any { !it.deleted }) {
SectionDividerSpaced()
- SectionView(generalGetString(MR.strings.operator_use_for_messages).uppercase()) {
+ SectionView(generalGetString(MR.strings.operator_use_for_messages)) {
SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(
stringResource(MR.strings.operator_use_for_messages_receiving),
@@ -306,7 +307,7 @@ fun OperatorViewLayout(
// Preset servers can't be deleted
if (userServers.value[operatorIndex].smpServers.any { it.preset }) {
SectionDividerSpaced()
- SectionView(generalGetString(MR.strings.message_servers).uppercase()) {
+ SectionView(generalGetString(MR.strings.message_servers)) {
userServers.value[operatorIndex].smpServers.forEachIndexed { i, server ->
if (!server.preset) return@forEachIndexed
SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.SMP) }) {
@@ -340,7 +341,7 @@ fun OperatorViewLayout(
if (userServers.value[operatorIndex].smpServers.any { !it.preset && !it.deleted }) {
SectionDividerSpaced()
- SectionView(generalGetString(MR.strings.operator_added_message_servers).uppercase()) {
+ SectionView(generalGetString(MR.strings.operator_added_message_servers)) {
userServers.value[operatorIndex].smpServers.forEachIndexed { i, server ->
if (server.deleted || server.preset) return@forEachIndexed
SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.SMP) }) {
@@ -356,7 +357,7 @@ fun OperatorViewLayout(
if (userServers.value[operatorIndex].xftpServers.any { !it.deleted }) {
SectionDividerSpaced()
- SectionView(generalGetString(MR.strings.operator_use_for_files).uppercase()) {
+ SectionView(generalGetString(MR.strings.operator_use_for_files)) {
SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(
stringResource(MR.strings.operator_use_for_sending),
@@ -389,7 +390,7 @@ fun OperatorViewLayout(
// Preset servers can't be deleted
if (userServers.value[operatorIndex].xftpServers.any { it.preset }) {
SectionDividerSpaced()
- SectionView(generalGetString(MR.strings.media_and_file_servers).uppercase()) {
+ SectionView(generalGetString(MR.strings.media_and_file_servers)) {
userServers.value[operatorIndex].xftpServers.forEachIndexed { i, server ->
if (!server.preset) return@forEachIndexed
SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.XFTP) }) {
@@ -423,7 +424,7 @@ fun OperatorViewLayout(
if (userServers.value[operatorIndex].xftpServers.any { !it.preset && !it.deleted}) {
SectionDividerSpaced()
- SectionView(generalGetString(MR.strings.operator_added_xftp_servers).uppercase()) {
+ SectionView(generalGetString(MR.strings.operator_added_xftp_servers)) {
userServers.value[operatorIndex].xftpServers.forEachIndexed { i, server ->
if (server.deleted || server.preset) return@forEachIndexed
SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.XFTP) }) {
@@ -490,7 +491,7 @@ fun OperatorInfoView(serverOperator: ServerOperator) {
}
}
- SectionDividerSpaced(maxBottomPadding = false)
+ SectionDividerSpaced()
val uriHandler = LocalUriHandler.current
SectionView {
@@ -507,7 +508,7 @@ fun OperatorInfoView(serverOperator: ServerOperator) {
val selfhost = serverOperator.info.selfhost
if (selfhost != null) {
- SectionDividerSpaced(maxBottomPadding = false)
+ SectionDividerSpaced()
SectionView {
SectionItemView {
val (text, link) = selfhost
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt
index 01630a2b52..b3326bd2e9 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt
@@ -5,6 +5,7 @@ import SectionDividerSpaced
import SectionItemView
import SectionItemViewSpaceBetween
import SectionView
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.*
@@ -80,7 +81,8 @@ fun ProtocolServerView(
)
}
}
- }
+ },
+ cardScreen = true,
) {
Box {
ProtocolServerLayout(
@@ -140,7 +142,7 @@ private fun PresetServer(
testing: Boolean,
testServer: () -> Unit
) {
- SectionView(stringResource(MR.strings.smp_servers_preset_address).uppercase()) {
+ SectionView(stringResource(MR.strings.smp_servers_preset_address)) {
SelectionContainer {
Text(
server.value.server,
@@ -172,7 +174,7 @@ fun CustomServer(
}
}
SectionView(
- stringResource(MR.strings.smp_servers_your_server_address).uppercase(),
+ stringResource(MR.strings.smp_servers_your_server_address),
icon = painterResource(MR.images.ic_error),
iconTint = if (!valid.value) MaterialTheme.colors.error else Color.Transparent,
) {
@@ -190,13 +192,13 @@ fun CustomServer(
}
}
}
- SectionDividerSpaced(maxTopPadding = true)
+ SectionDividerSpaced()
UseServerSection(server, valid.value, testing, testServer, onDelete)
if (valid.value) {
SectionDividerSpaced()
- SectionView(stringResource(MR.strings.smp_servers_add_to_another_device).uppercase()) {
+ SectionView(stringResource(MR.strings.smp_servers_add_to_another_device)) {
QRCode(serverAddress.value, small = true)
}
}
@@ -210,7 +212,7 @@ private fun UseServerSection(
testServer: () -> Unit,
onDelete: (() -> Unit)? = null,
) {
- SectionView(stringResource(MR.strings.smp_servers_use_server).uppercase()) {
+ SectionView(stringResource(MR.strings.smp_servers_use_server)) {
SectionItemViewSpaceBetween(testServer, disabled = !valid || testing) {
Text(stringResource(MR.strings.smp_servers_test_server), color = if (valid && !testing) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary)
ShowTestStatus(server.value)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt
index 3be2456b72..280cd7bedb 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt
@@ -7,9 +7,11 @@ import SectionItemView
import SectionTextFooter
import SectionView
import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.background
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
+import chat.simplex.common.ui.theme.*
import androidx.compose.ui.platform.LocalUriHandler
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@@ -86,7 +88,7 @@ fun YourServersViewLayout(
Column {
if (userServers.value[operatorIndex].chatRelays.any { !it.deleted }) {
val duplicateRelayAddresses = findDuplicateRelayAddresses(serverErrors.value)
- SectionView(generalGetString(MR.strings.chat_relays).uppercase()) {
+ SectionView(generalGetString(MR.strings.chat_relays)) {
userServers.value[operatorIndex].chatRelays.forEachIndexed { i, relay ->
if (relay.deleted) return@forEachIndexed
ChatRelayViewLink(relay, duplicateRelayAddresses) {
@@ -99,7 +101,7 @@ fun YourServersViewLayout(
if (userServers.value[operatorIndex].smpServers.any { !it.deleted }) {
SectionDividerSpaced()
- SectionView(generalGetString(MR.strings.message_servers).uppercase()) {
+ SectionView(generalGetString(MR.strings.message_servers)) {
userServers.value[operatorIndex].smpServers.forEachIndexed { i, server ->
if (server.deleted) return@forEachIndexed
SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.SMP) }) {
@@ -133,7 +135,7 @@ fun YourServersViewLayout(
if (userServers.value[operatorIndex].xftpServers.any { !it.deleted }) {
SectionDividerSpaced()
- SectionView(generalGetString(MR.strings.media_and_file_servers).uppercase()) {
+ SectionView(generalGetString(MR.strings.media_and_file_servers)) {
userServers.value[operatorIndex].xftpServers.forEachIndexed { i, server ->
if (server.deleted) return@forEachIndexed
SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.XFTP) }) {
@@ -170,7 +172,7 @@ fun YourServersViewLayout(
userServers.value[operatorIndex].xftpServers.any { !it.deleted } ||
userServers.value[operatorIndex].chatRelays.any { !it.deleted }
) {
- SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
+ SectionDividerSpaced()
}
SectionView {
@@ -195,7 +197,7 @@ fun YourServersViewLayout(
ServersWarningFooter(serversWarn)
}
}
- SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
+ SectionDividerSpaced()
SectionView {
TestServersButton(
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml
index 95ec53287a..d5af88c18b 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml
@@ -60,7 +60,7 @@
السماح لجهات اتصالك بالاتصال بك.السماح بردود الفعل على الرسائل.يتم مسح جميع البيانات عند إدخالها.
- سيتم استخدام Android Keystore لتخزين عبارة المرور بشكل آمن بعد إعادة تشغيل التطبيق أو تغيير عبارة المرور - سيسمح بتلقي الإشعارات.
+ سيتم استخدام Android Keystore لتخزين عبارة المرور بشكل آمن بعد إعادة تشغيل التطبيق أو تغيير عبارة المرور - سيسمح بإستلام الإشعارات.السماح لجهات اتصالك بإرسال رسائل تختفي.اسمح بالرسائل الصوتية فقط إذا سمحت جهة اتصالك بذلك.رمز مرور التطبيق
@@ -76,7 +76,7 @@
عن عنوان SimpleXبناء التطبيق: %sالمظهر
- أضف عنوانًا إلى ملف تعريفك، حتى تتمكن جهات اتصالك من مشاركته مع أشخاص آخرين. سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك.
+ أضف عنوانًا إلى ملف تعريفك، حتى تتمكن جهات اتصالك على SimpleX من مشاركته مع أشخاص آخرين. سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك على SimpleX.ستبقى جميع جهات اتصالك متصلة. سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك.رمز التطبيقعنوان
@@ -525,7 +525,7 @@
فوريالمضيفإخفاء
- السماح بذلك في مربع الحوار التالي لتلقي الإشعارات على الفور.]]>
+ السماح بذلك في مربع الحوار التالي لاستلام الإشعارات على الفور.]]>ردًا علىإشعارات فوريةخوادم ICE (واحد لكل سطر)
@@ -728,7 +728,7 @@
إيصالات تسليم الرسائل!دقائقشهور
- - توصيل رسائل أكثر استقرارًا.\n- مجموعات أفضل قليلاً.\n- و اكثر!
+ - تسليم رسائل أكثر استقرارًا.\n- مجموعات أفضل قليلاً.\n- و اكثر!حالة الشبكةكتمردود الفعل الرسائل ممنوعة.
@@ -878,7 +878,7 @@
يرى المُستلمون التحديثات أثناء كتابتها.استلمت، ممنوعحفظ
- سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك.
+ سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك على SimpleX.حفظ وإشعار جهات الاتصالحفظ وتحديث ملف تعريف المجموعةعدد البينج
@@ -971,7 +971,7 @@
تدمير ذاتيمسح الرمزأرسل أسئلة وأفكار
- مشاركة العنوان مع جهات الاتصال؟
+ مشاركة العنوان مع جهات اتصال SimpleX؟شارك العنوانحفظ رسالة الترحيب؟احفظ الخوادم
@@ -1060,7 +1060,7 @@
التوقف عن إرسال الملف؟عنوان SimpleXاستخدم مضيفي .onion إلى "لا" إذا كان وسيط SOCKS لا يدعمها.]]>
- مشاركة مع جهات الاتصال
+ شارك مع جهات اتصال SimpleXإيقاف التشغيل؟إعدادات وسيط SOCKSإيقاف التشغيل
@@ -1091,7 +1091,7 @@
للاتصال، يمكن لجهة الاتصال مسح رمز QR أو استخدام الرابط في التطبيق.اختبر الخوادملا معرّفات مُستخدم
- دعم SIMPLEX CHAT
+ دعم SimpleX Chatبدِّلالعنوان الرئيسيسيتم وضع علامة على الرسالة على أنها تحت الإشراف لجميع الأعضاء.
@@ -1306,7 +1306,7 @@
الإيصالات مُعطَّلة%s: %sتضم هذه المجموعة أكثر من %1$d عضو، ولا يتم إرسال إيصالات التسليم.
- التوصيل
+ التسليممُعطَّلتعطيل الإيصالات للمجموعات؟فعّل (الاحتفاظ بتجاوزات المجموعة)
@@ -1323,7 +1323,7 @@
افتح إعدادات التطبيقلا يمكن تشغيل SimpleX في الخلفية. ستستلم الإشعارات فقط عندما يكون التطبيق قيد التشغيل.سيتم مشاركة ملف تعريف عشوائي جديد.
- ألصق الرابط المُستلَم للتواصل مع جهة اتصالك…
+ ألصِق الرابط المُستلَم للتواصل مع جهة اتصالك…ستتم مشاركة ملفك التعريفي %1$s.قد يغلق التطبيق بعد دقيقة واحدة في الخلفية.سماح
@@ -1348,7 +1348,7 @@
افتح مجلد قاعدة البياناتسيتم تخزين عبارة المرور في الإعدادات كنص عادي بعد تغييرها أو إعادة تشغيل التطبيق.عبارة المرور مخزنة في الإعدادات كنص عادي.
- يُرجى الملاحظة: يتم توصيل مُرحلات الرسائل والملفات عبر وسيط SOCKS. تستخدم المكالمات وإرسال معاينات الروابط الاتصال المباشر.]]>
+ يُرجى الملاحظة: يتم تسليم مُرحلات الرسائل والملفات عبر وسيط SOCKS. تستخدم المكالمات الاتصال المباشر.]]>عَمِّ الملفات المحليةعمِّ الملفات والوسائط المخزنةتطبيق سطح المكتب الجديد!
@@ -1843,7 +1843,7 @@
العضو غير نشطرسالة مُحوّلةلا يوجد اتصال مباشر حتى الآن، الرسالة مُحوّلة بواسطة المُدير.
- امسح / ألصِق الرابط
+ ألصِق رابط / امسحخوادم SMP المهيأةخوادم SMP أخرىخوادم XFTP المهيأة
@@ -1936,7 +1936,7 @@
أعِد توصيل كافة الخوادم المتصلة لفرض تسليم الرسالة. يستخدم حركة مرور إضافية.خوادم موّكلةتلقي الأخطاء
- تلقى الإجمالي
+ استلام الإجماليأعِد توصيل جميع الخوادمأعِد توصيل الخوادم؟عنوان الخادم
@@ -2125,7 +2125,7 @@
%s.]]>شروط الاستخدامللتوجيه الخاص
- لتلقي
+ لاستلاماستخدم للملفاتاعرض الشروط%s.]]>
@@ -2159,12 +2159,12 @@
لا يمكن تحميل نص الشروط الحالية، يمكنك مراجعة الشروط عبر هذا الرابط:خطأ في قبول الشروطخطأ في حفظ الخوادم
- على سبيل المثال، إذا تلقى أحد جهات اتصالك رسائل عبر خادم SimpleX Chat، فسيقوم تطبيقك بتسليمها عبر خادم Flux.
+ على سبيل المثال، إذا استلم أحد جهات اتصالك رسائل عبر خادم SimpleX Chat، فسيقوم تطبيقك بتسليمها عبر خادم Flux.لا يوجد خوادم لتوجيه الرسائل الخاصة.لا يوجد خوادم رسائل.
- لا يوجد خوادم لاستقبال الملفات.
+ لا يوجد خوادم لاستلام الملفات.لا توجد رسالة
- لا يوجد خوادم لاستقبال الرسائل.
+ لا يوجد خوادم لاستلام الرسائل.- فتح الدردشة عند أول رسالة غير مقروءة.\n- الانتقال إلى الرسائل المقتبسة.يمكنك تعيين اسم الاتصال، لتذكر الأشخاص الذين تمت مشاركة الرابط معهم.راجع لاحقًا
@@ -2197,7 +2197,7 @@
احذف الدردشةالدردشة موجودة بالفعل!حذف الدردشة؟
- %1$s.]]>
+ %1$s.]]>أو استورد ملف الأرشيفلا توجد خدمة خلفيةالإشعارات والبطارية
@@ -2350,8 +2350,8 @@
لا يمكن قراءة عبارة المرور في Keystore. قد يكون هذا قد حدث بعد تحديث النظام غير متوافق مع التطبيق. إذا لم يكن الأمر كذلك، فيُرجى التواصل مع المطوِّرين.موافقة الانتظارسياسة الخصوصية وشروط الاستخدام.
- لا يمكن الوصول إلى الدردشات الخاصة والمجموعات وجهات اتصالك لمشغلي الخادم.
- باستخدام SimpleX Chat، توافق على:\n- إرسال المحتوى القانوني فقط في المجموعات العامة.\n- احترام المستخدمين الآخرين – لا سبام.
+ يلتزم المشغلون بما يلي:\n- الاستقلالية\n- تقليل استخدام البيانات الوصفية\n- تشغيل برمجيات مفتوحة المصدر وموثقة
+ أنت تلتزم بـ:\n- المحتوى القانوني فقط في المجموعات العامة\n- احترام المستخدمين الآخرين — لا للرسائل المزعجةاقبليتطلب هذا الرابط إصدار تطبيق أحدث. يُرجى ترقية التطبيق أو اطلب من جهة اتصالك إرسال رابط متوافق.رابط كامل
@@ -2564,7 +2564,7 @@
حُدِّث ملف تعريف القناةستُحذف القناة لجميع المشتركين - لا يمكن التراجع عن هذا الإجراء!ستُحذف القناة من عِندك - لا يمكن التراجع عن هذا الإجراء!
- ستبدأ القناة بالعمل مع %1$d من أصل %2$d من المُرحلات. أتود المتابعة؟
+ ستبدأ القناة بالعمل مع %1$d من أصل %2$d مُرحلات. أتود الاستمرار؟مُرحل الدردشةمُرحلات الدردشةمُرحلات الدردشة توجّه الرسائل في القنوات التي تنشئها.
@@ -2654,4 +2654,160 @@
عنوان مُرحلكاسم مُرحلكستتوقف عن تلقي الرسائل من هذه القناة، وسيتم الاحتفاظ بسجل الدردشة.
+ %1$d/%2$d مِن المُرحلات نشطة، و%3$d أخطاء
+ %1$d/%2$d مِن المُرحلات نشطة، وأُزيل %3$d
+ %1$d/%2$d مِن المُرحلات متصلة، وفشل %3$d
+ %1$d/%2$d مِن المُرحلات متصلة، وأُزيل %3$d
+ فشل %1$d مُرحلات
+ %1$d مُرحلات غير نشطة
+ %1$d مُرحلات أُزيلت
+ أضف مُرحلات لاستعادة تسليم الرسائل.
+ رابط اتصال لشخص واحد
+ اسمح للأعضاء بالدردشة مع المُدراء.
+ اسمح بإرسال رسائل مباشرة للمشتركين.
+ اسمح للمشتركين بالدردشة مع المُدراء.
+ فشلت كل المُرحلات
+ أُزيلت كل المُرحلات
+ لأننا دمرنا القدرة على معرفة من أنت. لكي لا تُسلب قوتك أبدًا.
+ كن حُرًا\nفي شبكتك
+ كن حُرًا في شبكتك.
+ الشريط السفلي
+ عنوان العمل التجاري
+ تعذّر الإذاعة
+ القناة لا تحتوي على محطات تقوية نشطة. يُرجى محاولة الانضمام لاحقًا.
+ رابط القناة
+ تفضلات القناة
+ القنوات
+ القناة غير متوفرة مؤقتًا
+ الدردشة مع المُدراء محظورة.
+ الدردشة مع المُدراء في القنوات العامة لا تتوفر فيها ميزة التعمية بين الطرفين (E2E) - لا تستخدمها إلا مع مُرحلات دردشة موثوقة.
+ عُطّل الدردشة مع الأعضاء
+ الدردشة مع المُدراء
+ اتصل عبر رابط أو رمز QR
+ عنوان التواصل
+ أنشئ رابطك
+ أنشئ رابطك العام
+ تُمنع الرسائل المباشرة بين المشتركين.
+ عطّل
+ لا ترسل السجل للمشتركين الجدد.
+ أصبح أسهل أن تدعي أصحابك 👋
+ فعّل
+ فعّل
+ فعّل الدردشة مع المُدراء؟
+ فعّل معاينة الروابط؟
+ أدخل اسم ملف التعريف…
+ خطأ
+ خطأ في مشاركة القناة
+ لكي يتمكن أي شخص من الوصول إليك
+ (مِن المالك)
+ ابدأ
+ رابط المجموعة
+ لا يُرسل السجل للمشتركين الجدد.
+ غير نشط
+ ادعُ شخصٍ ما بشكل خاص
+ اجعل شخصًا ما يتواصل معك
+ سيتم طلب معاينة الرابط عبر وسيط SOCKS. قد لا يزال البحث عن نظام أسماء النطاقات (DNS) يتم محليًا من خلال محلّل DNS الخاص بك.
+ تحققَ من توقيع الرابط.
+ يمكن للأعضاء الدردشة مع المُدراء.
+ ليست مُعمّاة تمامًا بين الطرفين. يمكن لمُرحلات الدردشة رؤية هذه الرسائل.]]>
+ رحّل
+ التزامات الشبكة
+ خطأ في الشبكة
+ لا يمكن لموجّهات الشبكة معرفة مَن يتحدث مع مَن
+ رابط جديد لمرة واحدة
+ لا حساب. لا هاتف. لا بريد إلكتروني. لا هوية. التعمية الأكثر أمانًا.
+ لا مُرحلات نشطة
+ لم يتتبع أحد محادثاتك. ولم يرسم أحد خريطة للأماكن التي زرتها. لم تكن الخصوصية مجرد ميزة، بل كانت أسلوب حياة.
+ تنظيم غير ربحي
+ ليس مجرد قفل أفضل على باب شخص آخر. ولا صاحب عقار ألطف يحترم خصوصيتك، لكنه لا يزال يحتفظ بسجل لجميع الزوار. أنت لست ضيفًا. أنت في بيتك. لا يمكن لأي ملك أن يدخله — فأنت صاحب السيادة.
+ رابط لمرة واحدة
+ يمكن لمالكي القنوات فقط تغيير تفضيلات القناة.
+ على جوّالك، وليس على الخوادم.
+ افتح رابط خارجي؟
+ - الموافقة على إرسال معاينات الروابط.\n- استخدام وسيط SOCKS في حال تفعيله.\n- منع التصيد الاحتيالي عبر الروابط التشعبية.\n- إزالة تتبُع الروابط.
+ أو أظهر رمز QR شخصيًا أو عبر مكالمة فيديو.
+ أو استخدم رمز QR هذا - اطبعه أو اعرضه عبر الإنترنت.
+ الملكية: يمكنك تشغيل المُرحلات الخاصة بك.
+ الخصوصية: للمالكين والمشتركين.
+ مُراسلة خاصة وآمنة.
+ منع الدردشات مع المُدراء.
+ منع إرسال رسائل مباشرة للمشتركين.
+ قنوات عامة - تحدث بحرية 🚀
+ نتائج المُرحل:
+ الموثوقية: عِدّة مُرحلات لكل قناة.
+ أُزيل بواسطة المُشغل
+ روابط ويب آمنة
+ الأمن: المالكون يمتلكون مفاتيح القنوات.
+ قد يؤدي إرسال معاينة للرابط إلى كشف عنوان IP الخاص بك للموقع الإلكتروني. يمكنك تغيير هذا الإعداد لاحقًا من إعدادات الخصوصية.
+ أرسل الرابط عبر أي تطبيق مُراسلة - فهو آمن. واطلب منه لصقه في SimpleX.
+ أرسل ما يصل إلى آخر 100 رسالة للمشتركين الجدد.
+ أعِدّ الإشعارات
+ أعِدّ أجهزة التوجيه
+ شارك القناة…
+ شارك عبر الدردشة
+ ⚠️ فشل التحقق من التوقيع: %s.
+ (موقّع)
+ بلاغات المشترك
+ يمكن للمشتركين إضافة ردود الفعل على الرسائل.
+ يمكن للمشتركين الدردشة مع المُدراء.
+ يمكن للمشتركين حذف الرسائل المُرسلة نهائيًا. (24 ساعة)
+ يمكن للمشتركين الإبلاغ عن الرسائل للمشرفين.
+ يمكن للمشتركين إرسال رسائل مباشرة.
+ يمكن للمشتركين إرسال رسائل تختفي.
+ يمكن للمشتركين إرسال الملفات والوسائط.
+ يمكن للمشتركين إرسال روابط SimpleX.
+ يمكن للمشتركين إرسال رسائل صوتية.
+ تحدث مع شخصٍ ما
+ انقر للفتح
+ وصل الاتصال إلى الحد الأقصى للرسائل غير المُسلَّمة
+ أول شبكة تمتلك\nفيها جهات اتصالك ومجموعاتك.
+ ’ثم انتقلنا إلى الإنترنت، وطلبت كل منصة جزءًا منك؛ اسمك، ورقمك، وأصدقاءك. وقبلنا بأن ثمن التحدث مع الآخرين هو السماح لشخص ما بمعرفة مَن نتحدث إليهم. وكل جيل، سواء من الناس أو التقنية، عاش الأمر على هذا النحو؛ الهاتف، والبريد الإلكتروني، وتطبيقات المراسلة، ووسائل التواصل الاجتماعي. وبدا أن هذه هي الطريقة الوحيدة الممكنة.
+ أقدم حرية إنسانية — وهي التحدث إلى شخص آخر دون مراقبة — مبنية على بنية تحتية لا يمكنها خيانتها.
+ هناك طريقة أخرى. شبكة بلا أرقام هواتف، ولا أسماء مستخدمين، ولا حسابات، ولا هويات مستخدمين من أي نوع. شبكة تربط الناس وتنقل الرسائل المُعمّاة دون معرفة مَن المتصل.
+ لضمان استمرارية شبكة SimpleX.
+ الشريط العلوي
+ يُرسل ما يصل إلى آخر 100 رسالة للمشتركين الجدد.
+ استخدم هذا العنوان في ملف تعريفك على مواقع التواصل الاجتماعي أو موقعك الإلكتروني أو في توقيع بريدك الإلكتروني.
+ في انتظار قيام مالك القناة بإضافة المُرحلات.
+ جعلنا عملية الاتصال أكثر بساطة للمستخدمين الجدد.
+ لماذا بُنيا SimpleX.
+ محادثاتك ملك لك، تمامًا كما كان الحال دائمًا قبل ظهور الإنترنت. الشبكة ليست مكانًا تزوره، بل هي مكان تصنعه وتمتلكه؛ ولا يمكن لأحد أن يسلبه منك، سواء جعلته خاصًا أو عامًا.
+ شبكتك
+ ملف تعريفك
+ عنوانك العام
+ لقد وُلدت دون حساب تعريفي.
+ قناتك الجديدة %1$s متصلة بـ %2$d من أصل %3$d مُرحلات. إذا ألغيت، ستُحذف القناة - يمكنك إنشاؤها مرة أخرى.
+ أضف
+ أضف مُرحل
+ أضف مُرحلات
+ ألغِ واحذف القناة
+ %d مُرحل/ات مُحدّدة
+ خطأ في إضافة المُرحلات
+ لا مُرحلات متاحة
+ لا مُرحلات
+ لا مُرحلات مُحدّدة
+ المُرحلات المُضافة: %1$s.
+ سيُزيل المُرحل من القناة - لا يمكن التراجع عن هذا الإجراء!
+ أُزيل
+ أزِل المُرحل
+ إزالة المُرحل؟
+ حدّد المُرحلات
+ هذا هو آخر مُرحل نشط. إزالته ستمنع تسليم الرسائل للمشتركين.
+ أغلِق التطبيق
+ خطأ في حذف الرسالة
+ من السجل
+ إذا اخترت أغلِق، فلن تُستلم الرسائل.\nيمكنك تغيير ذلك لاحقًا من إعدادات المظهر.
+ أبقِ SimpleX يعمل في الخلفية لاستلام الرسائل.
+ صغّر إلى اللوحة
+ تصغير إلى اللوحة؟
+ صغّر إلى اللوحة عند إغلاق النافذة
+ أنهِ SimpleX
+ أظهر SimpleX
+ SimpleX
+ SimpleX — %d غير مقروءة
+ قد تكون هناك نُسخة أخرى من التطبيق قيد التشغيل أو لم تُغلق بشكل صحيح. أتريد البدء على أي حال؟
+ التطبيق يعمل بالفعل
+ رُفض
+ رُفض بواسطة مُشغل المُرحل
+ الحالة
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
index 375edecd44..9630c61004 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
@@ -194,8 +194,15 @@
Please check that you used the correct link or ask your contact to send you another one.Unsupported connection linkThis link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.
+ Unsupported channel name
+ Unsupported contact name
+ Connecting via channel name requires a newer app version.
+ Connecting via contact name requires a newer app version.
+ Please upgrade the app.Channel temporarily unavailableChannel has no active relays. Please try to join later.
+ App update required
+ This group requires a newer version of the app. Please update the app to join.Connection error (AUTH)Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection.Connection blocked
@@ -1101,7 +1108,7 @@
OffAppearanceCustomize theme
- INTERFACE COLORS
+ Interface colorsApp versionApp version: v%sApp build: %s
@@ -1537,26 +1544,33 @@
Open clean link
- YOU
- SETTINGS
- CHAT DATABASE
- HELP
- SUPPORT SIMPLEX CHAT
- APP
- DEVICE
- CHATS
- FILES
- SEND DELIVERY RECEIPTS TO
- CONTACT REQUESTS FROM GROUPS
+ You
+ Settings
+ Chat database
+ Help
+ Support SimpleX Chat
+ App
+ Device
+ Chats
+ Files
+ Send delivery receipts to
+ Contact requests from groups
+ About
+ Contact
+ Support the project
+ Chat data
+ Help & support
+ More privacy
+ Advanced settingsRestartShutdownDeveloper toolsExperimental features
- SOCKS PROXY
- INTERFACE
+ SOCKS proxy
+ InterfaceLANGUAGE
- APP ICON
- THEMES
+ App icon
+ ThemesProfile imagesMessage shapeCorner
@@ -1564,21 +1578,21 @@
Chat themeProfile themeChat colors
- MESSAGES AND FILES
- PRIVATE MESSAGE ROUTING
- CALLS
+ Messages and files
+ Private message routing
+ CallsNetwork connectionIncognito mode
- EXPERIMENTAL
+ ExperimentalUse from desktopYour chat database
- RUN CHAT
+ Run chatRemote mobilesChat is runningChat is stopped
- CHAT DATABASE
+ Chat databaseDatabase passphraseExport databaseImport database
@@ -1887,7 +1901,7 @@
Invite membersAdd team membersAdd friends
- %1$s MEMBERS
+ %1$s membersyou: %1$sDelete groupDelete channel
@@ -1911,6 +1925,20 @@
Welcome messageGroup linkChannel link
+ Channel webpage
+ Group webpage
+ Advanced options
+ https://
+ Allow anyone to embed
+ Enter webpage URL
+ It will be shown to subscribers and used to allow loading the preview.
+ Webpage code
+ Add this code to your webpage. It will display the preview of your channel / group.
+ Copy code
+ Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting.
+ Used chat relays do not support webpages.
+ Any webpage can show the preview.
+ Only your page above can show the preview.Create group linkCreate linkDelete link?
@@ -1940,7 +1968,7 @@
Chat relays
- FOR CONSOLE
+ For consoleLocal nameDatabase IDDebug delivery
@@ -2010,7 +2038,7 @@
disabledfailedinactive
- MEMBER
+ MemberRoleChange roleChange
@@ -2027,7 +2055,7 @@
GroupChatConnection
- CONNECTION FAILED
+ Connection faileddirectindirect (%1$s)Message queue info
@@ -2056,7 +2084,7 @@
Message too large
- SERVERS
+ ServersReceiving viaSending viaNetwork status
@@ -2674,7 +2702,7 @@
Don\'t enableYou can enable later via SettingsDelivery receipts are disabled!
- You can enable them later via app Privacy & Security settings.
+ You can enable them later via app Your privacy settings.Error enabling delivery receipts!
@@ -3020,9 +3048,9 @@
Waiting for channel owner to add relays.
- RELAY
- OWNER
- SUBSCRIBER
+ Relay
+ Owner
+ SubscriberChannelRelay linkRelay address
@@ -3096,6 +3124,14 @@
Quit SimpleXSimpleXSimpleX — %d unread
- Minimize to tray when closing window
- Keep SimpleX running in the background to receive messages.
+ Close to tray
+ Runs in background to receive messages
+ %s supports SimpleX Chat.
+ %1$s supported SimpleX Chat. The badge expired on %2$s.
+ You can support SimpleX starting from v7 of the app.
+ %s invested in SimpleX Chat crowdfunding.
+ Unverified badge
+ This badge could not be verified and may not be genuine.
+ Badge cannot be verified
+ The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge.
\ No newline at end of file
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml
index c691447b32..890720a727 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml
@@ -83,7 +83,7 @@
Android Keystore се използва за сигурно съхраняване на паролата - тоа позволява на услугата за известия да работи.Създаен беше празен профил за чат с предоставеното име и приложението се отвари както обикновено.Приложението може да получава известия само когато работи, няма да се стартира услуга във фонов режим
- ИКОНА НА ПРИЛОЖЕНИЕТО
+ Икона на приложениетоИдентифицирайОптимизацията на батерията е активна, изключват се фоновата услуга и периодичните заявки за нови съобщения. Можете да ги активирате отново през настройките.за всеки чат профил, който имате в приложението.]]>
@@ -169,13 +169,13 @@
чрез релевидео разговорВашите обаждания
- ПРИЛОЖЕНИЕ
+ ПриложениеРезервно копие на данните от приложениетоКода за достъп до приложение се заменя с код за самоунищожение.Автоматично приемане на изображенияИдентификацията е отмененаИзпрати визуализация на линковете
- ОБАЖДАНИЯ
+ ОбажданияAndroid Keystore ще се използва за сигурно съхраняване на паролата, след като рестартирате приложението или промените паролата - това ще позволи получаването на известия.Промяна на паролата на базата данни\?променена ролята от %s на %s
@@ -223,7 +223,7 @@
Базата данни е изтритаЧатът работиЧатът е спрян
- БАЗА ДАННИ
+ База данниБазата данни е импортиранаПотвърди новата парола…Потвърди актуализаациите на базата данни
@@ -314,13 +314,13 @@
Промяна на режима на заключванеПромени режима на самоунищожениеПромени кода за достъп за самоунищожение
- ЧАТОВЕ
+ Чатовепромяна на адреса…В момента максималният поддържан размер на файла е %1$s.ID в базата данниID в базата данни: %dКонтакти
- ТЕМИ
+ ТемиБазата данни е криптирана с автоматично генерирана парола. Моля, променете я преди експортиране.Парола за базата данниИзтрий базата данни
@@ -406,7 +406,7 @@
Идентификатори в базата данни и опция за изолация на транспорта.Изтрий адресИзтрий адрес\?
- ЦВЕТОВЕ НА ИНТЕРФЕЙСА
+ Цветове на интерфейсаСъздайСъздай профилИзтрий изображение
@@ -446,7 +446,7 @@
Активирай потвърждениeто\?Изпращането на потвърждениe за доставка е деактивирано за %d контактаИзпращането на потвърждениe е активирано за %d контакта
- УСТРОЙСТВО
+ УстройствоДеактивиране (запазване на промените)%d файл(а) с общ размер от %sКриптирай
@@ -485,7 +485,7 @@
Те могат да бъдат променени в настройките за всеки контакт и група.Инструменти за разработчициДеактивиране за всички
- ИЗПРАЩАЙТЕ ПОТВЪРЖДЕНИE ЗА ДОСТАВКА НА
+ Изпращайте потвърждениe за доставка наИзтрий съобщенията след%s секунда(и)Изтрий съобщенията
@@ -622,7 +622,7 @@
Пълно име:Изход без запазванеПарола за скрит профил
- ЕКСПЕРИМЕНТАЛЕН
+ ЕксперименталенФайл: %sРазшири избора на роляПоправи връзката\?
@@ -632,7 +632,7 @@
Изпратените съобщения ще бъдат изтрити след зададеното време.Групов линкФайлове и медия
- ЗА КОНЗОЛАТА
+ За конзолатаГрупови настройкиФайлФайлът не е намерен
@@ -643,7 +643,7 @@
Филтрирайте непрочетените и любимите чатове.Членовете могат да изпращат лични съобщения.помощ
- ПОМОЩ
+ ПомощЗдравей,
\nСвържи се с мен през SimpleX Chat: %sЧленовете могат да добавят реакции към съобщенията.
@@ -805,7 +805,7 @@
Когато приложението работиПериодичноПостави получения линк
- СЪОБЩЕНИЯ И ФАЙЛОВЕ
+ Съобщения и файловеНяма получени или изпратени файловеИзвестията ще се доставят само докато приложението не е спряно!Премахване на парола от Keystore\?
@@ -987,7 +987,7 @@
Режим на заключванеМоля, докладвайте го на разработчиците.Защити екрана на приложението
- ЧЛЕН
+ ЧленПремахванеPING бройкаСамо вашият контакт може да добавя реакции на съобщенията.
@@ -1042,8 +1042,8 @@
Сподели с контактитеСпри споделянетоСпри споделянето на адреса\?
- НАСТРОЙКИ
- СТАРТИРАНЕ НА ЧАТ
+ Настройки
+ Стартиране на чатЗадай име на контакт…Няма информация за доставкатаОтзови файл\?
@@ -1067,7 +1067,7 @@
Изпращането на потвърждениe за доставка е разрешено за %d групиРестартирайте приложението, за да използвате импортирана база данни.Тази група има над %1$d членове, потвърждениeто за доставка няма да се изпраща.
- СЪРВЪРИ
+ Сървъри%s: %sДоставкаАктивиране (запазване на груповите промени)
@@ -1093,7 +1093,7 @@
Сподели медия…SimpleX адресСигурността на SimpleX Chat беше одитирана от Trail of Bits.
- SOCKS ПРОКСИ
+ SOCKS проксиРестартиранеИзключванеРестартирайте приложението, за да създадете нов чат профил.
@@ -1152,7 +1152,7 @@
Заглавие(за споделяне с вашия контакт)Тази група вече не съществува.
- ПОДКРЕПЕТЕ SIMPLEX CHAT
+ Подкрепете SimpleX ChatВашият контакт изпрати файл, който е по-голям от поддържания в момента максимален размер (%1$s).Тази функция все още не се поддържа. Опитайте следващата версия.Докосни за започване на нов чат
@@ -1231,7 +1231,7 @@
адреса за получаване е промененМожете да споделите този адрес с вашите контакти, за да им позволите да се свържат с %s.Премахни от любимите
- ВИЕ
+ ВиеВашата база данниИзчаква се получаването на изображениетоИзчаква се получаването на изображението
@@ -1775,7 +1775,7 @@
За да защити вашия IP адрес, поверително рутиране използва вашите SMP сървъри за доставяне на съобщения.Препращане на съобщенията без файловете?Неизвестни сървъри
- ФАЙЛОВЕ
+ ФайловеПоказване на списъка на чатовете в нов прозорецСистемнаТъмна
@@ -1800,7 +1800,7 @@
Препращащ сървър: %1$s\nГрешка: %2$sВерсията на сървъра е несъвместима с мрежовите настройки.Защити IP адреса
- ПОВЕРИТЕЛНО РУТИРАНЕ НА СЪОБЩЕНИЯ
+ Поверително рутиране на съобщенияПриложението ще поиска потвърждение за изтегляния от неизвестни файлови сървъри (с изключение на .onion сървъри или когато SOCKS прокси е активирано).Грешка: %1$sИзтегляне
@@ -2038,7 +2038,7 @@
Изтегли %s (%s)Пропусни тази версияПровери за актуализации
- БАЗА ДАННИ
+ База данниМожете да изпращате съобщения до %1$s от архивираните контакти.Достъпен панелИзпращането на съобщения на груповия член не е налично
@@ -2501,7 +2501,7 @@
Сподели стар линкЛинкът ще бъде кратък и профилът на групата ще бъде споделен чрез него.Обнови групов линк
- ЗАЯВКИ ЗА КОНТАКТ ОТ ГРУПИ
+ Заявки за контакт от групиЧленът е изтрит - не може да се приеме заявкатазаявка за връзка от група %1$sТази настройка е за текущия профил
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml
index 7ab3f5a381..c53247853f 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml
@@ -126,10 +126,10 @@
S\'eliminaran totes les dades de l\'aplicació.Es crea un perfil de xat buit amb el nom proporcionat i l\'aplicació s\'obre com de costum.La contrasenya de l\'aplicació es substitueix per una contrasenya d\'autodestrucció.
- APLICACIÓ
- ICONA APLICACIÓ
+ Aplicació
+ Icona aplicacióDesenfocar els mitjans
- TRUCADES
+ TrucadesAndroid Keystore s\'utilitza per emmagatzemar de manera segura la frase de contrasenya: permet que el servei de notificacions funcioni.Android Keystore s\'utilitzarà per emmagatzemar de manera segura la frase de contrasenya després de reiniciar l\'aplicació o canviar la frase de contrasenya; permetrà rebre notificacions.No es pot accedir a Keystore per desar la contrasenya de la base de dades
@@ -383,7 +383,7 @@
Desactivar rebuts?Desactivar rebuts per a grups?Eines per a desenvolupadors
- DISPOSITIU
+ DispositiuLa base de dades es xifra amb una contrasenya aleatòria. Si us plau, canvieu-la abans d\'exportar.Contrasenya de la base de dadesVoleu suprimir el perfil?
@@ -514,8 +514,8 @@
Canvia el mode l\'autodestruccióCanvia el codi d\'autodestruccióConfirmeu el codi d\'accés
- BASE DE DADES DELS XATS
- XATS
+ Base de dades dels xats
+ XatsTema del xatColors del xatBase de dades suprimida
@@ -551,7 +551,7 @@
Obre a l\'aplicació mòbil.]]>Error en desar els servidors ICEError en desar el servidor intermediari
- BASE DE DADES DELS XATS
+ Base de dades dels xatsEl xat s\'està executantEl xat està aturatError: %s
@@ -742,7 +742,7 @@
s\'està connectant…Les condicions s\'acceptaran per als operadors habilitats després de 30 dies.SimpleX no pot funcionar en segon pla. Només rebreu les notificacions quan obriu l\'aplicació.
- Trucades de SimpleX chat
+ Trucades de SimpleX ChatMissatges de xat de SimpleXenviatper llegir
@@ -781,7 +781,7 @@
Mostra:Amaga:SimpleX
- La seguretat de SimpleX chat ha estat auditada per Trail of Bits.
+ La seguretat de SimpleX Chat ha estat auditada per Trail of Bits.Parlem a SimpleX ChatEl nom no és vàlid!cursiva
@@ -794,7 +794,7 @@
inactiuMode clarGrups d\'incògnit
- MEMBRE
+ MembreVoleu unir-vos al grup?SurtVoleu sortir del xat?
@@ -966,8 +966,8 @@
Activar els rebuts?Activar autodestruccióActivar els rebuts per a grups?
- FITXERS
- EXPERIMENTAL
+ Fitxers
+ ExperimentalExportar base de dadesXifrarFitxer: %s
@@ -981,7 +981,7 @@
Grup no trobat!es requereix renegociar el xifratgegrup esborrat
- PER A CONSOLA
+ Per a consolaArreglar connexió?Correcció no suportada per membre del grupNom complet del grup:
@@ -1055,7 +1055,7 @@
Donar permís(os) per fer trucadesAuricularsXifra fitxers locals
- AJUT
+ AjutArxius i mitjansXifrar base de dades?Base de dades xifrada
@@ -1109,7 +1109,7 @@
MissatgeEl missatge és massa llarg!missatge
- MISSATGES I FITXERS
+ Missatges i fitxersMissatgesEstat del missatgeEstat del missatge: %s
@@ -1266,15 +1266,15 @@
L\'enviament de rebuts està desactivat per a %d contactesL\'enviament de rebuts està habilitat per a %d contactesL\'enviament de rebuts està habilitat per a %d grups
- ENVIAR ELS REBUS DE LLIURAMENT A
+ Enviar els rebus de lliurament aL\'enviament de rebuts està desactivat per a %d grupsReiniciar
- SERVIDOR INTERMEDIARI SOCKS
+ Servidor intermediari SOCKSImatges de perfil
- TEMES
+ TemesCuaForma del missatge
- EXECUTAR SIMPLEX
+ Executar SimpleXUsar des d\'ordinadorBase de dades de xatImportar base de dades
@@ -1899,7 +1899,7 @@
Utilitzar servidor intermediari SOCKS?Si disponiblesLes vostres credencials es podrien enviar sense xifrar.
- COLORS DE LA INTERFÍCIE
+ Colors de la interfícieAlternativa d\'encaminament de missatgesMode d\'encaminament de missatgesObrir ubicació del fitxer
@@ -1949,12 +1949,12 @@
Aquesta configuració és per al vostre perfil actualEs pot canviar a la configuració de contacte i grup.No
- CONFIGURACIÓ
+ ConfiguracióTouFort
- SUPORT SIMPLEX XAT
+ Suport SimpleX ChatConnexió a la xarxa
- ENCAMINAMENT DE MISSATGES PRIVAT
+ Encaminament de missatges privatmaiNo s\'han rebut ni enviats fitxersReinicieu l\'aplicació per crear un perfil de xat nou.
@@ -2024,7 +2024,7 @@
Vista prèviaRebent viaDesa i actualitza el perfil del grup
- SERVIDORS
+ ServidorsMissatge de benvingudaEl missatge de benvinguda és massa llargEls teus servidors
@@ -2183,7 +2183,7 @@
Servidors SMPServidors XFTPAltaveu
- VÓS
+ Vós%s segon(s)"Heu estat convidat a un grup"%s connectat
@@ -2474,7 +2474,7 @@
Compartir l\'enllaç anticL\'enllaç serà curt i el perfil del grup es compartirà a través d\'ell.Actualitzar l\'enllaç del grup
- SOL·LICITUDS DE CONTACTE DE GRUPS
+ Sol·licituds de contacte de grupsMembre eliminat(da); no es pot acceptar la sol·licitud.connexió sol·licitada del grup %1$sAquesta configuració és per al perfil actual
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml
index df4907885c..71c6db7b99 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml
@@ -40,11 +40,11 @@
Vytvořit odkazSmazat odkaz\?Odeslat přímou zprávu
- ČLEN
+ ČlenZměnit roli ve skupině\?Připojnepřímé (%1$s)
- SERVERY
+ ServeryPříjímáno přesVytvoření tajné skupinyZadejte název skupiny:
@@ -259,16 +259,16 @@
Reproduktor zapnutProbíhající hovorAutomaticky přijímat obrázky
- NASTAVENÍ
- NÁPOVĚDA
- ZAŘÍZENÍ
- KONVERZACE
+ Nastavení
+ Nápověda
+ Zařízení
+ KonverzaceExperimentální funkce
- SOCKS PROXY
- IKONA APLIKACE
- TÉMATA
- ZPRÁVY A SOUBORY
- VOLÁNÍ
+ SOCKS proxy
+ Ikona aplikace
+ Témata
+ Zprávy a soubory
+ VoláníExport databázeImport databázeSmazat databázi
@@ -700,15 +700,15 @@
\n1. Zprávy vypršely v odesílajícím klientovi po 2 dnech nebo na serveru po 30 dnech.
\n2. Dešifrování zprávy se nezdařilo, protože vy nebo váš kontakt jste použili starou zálohu databáze.
\n3. Spojení je kompromitováno.
- VY
- PODPOŘIT SIMPLEX CHAT
+ Vy
+ Podpořit SimpleX ChatNástroje pro vývojářeInkognito módVaše chat databáze
- SPUSTIT CHAT
+ Spustit chatChat je spuštěnChat je zastaven
- DATABÁZE CHATU
+ Databáze chatupřístupová fráze k databáziArchiv nové databázeArchiv staré databáze
@@ -836,7 +836,7 @@
Chyba při vytváření odkazu skupinyChyba při odstraňování odkazu skupinyPředvolby skupiny mohou měnit pouze vlastníci skupiny.
- PRO KONSOLE
+ Pro konsoleMístní názevID databázeOdstranit člena
@@ -991,7 +991,7 @@
Zvýšit a otevřít chatSkrýt:Zobrazit možnosti vývojáře
- POKUSNÝ
+ PokusnýObrázek bude přijat, až kontakt dokončí jeho nahrání.Zobrazit:ID databáze a možnost Izolace přenosu.
@@ -1164,7 +1164,7 @@
Když někdo požádá o připojení, můžete žádost přijmout nebo odmítnout.Uživatelské příručce.]]>Adresa SimpleX
- BARVY MOTIVU
+ Barvy motivuPřizpůsobit motivAktualizace profilu bude zaslána vašim kontaktům.Sdílet adresu s kontakty?
@@ -1301,7 +1301,7 @@
V odpovědi naŽádná historieČasový limit protokolu na KB
- ZASLAT POTVRZENÍ O DORUČENÍ NA
+ Zaslat potvrzení o doručení naDruhé zaškrtnutí jsme přehlédli! ✅Přijímací adresa bude změněna na jiný server. Změna adresy bude dokončena po připojení odesílatele.Vybrat soubor
@@ -1537,7 +1537,7 @@
Chyba vytváření zprávyChyba odstranění soukromých poznámekSmazat soukromé poznámky?
- Všechny zprávy budou smazány - nemůže být zvráceno!
+ Všechny zprávy budou smazány - nelze zvrátit!Možnosti vývojářeblokováno %skontakt %1$s změnen na %2$s
@@ -1556,8 +1556,7 @@
PC má nepodporovanou verzi. Ujistěte se, že používáte stejnou verzi na obou zařízeníchPC má chybný kód pozvánkyNeplatné jméno!
- Databáze migrace běží.
-\nMůže to trvat několik minut.
+ Probíhá migrace databáze. \nMůže to trvat několik minut.člen %1$s změněn na %2$sblokovánoBlokováno adminem
@@ -1589,7 +1588,7 @@
%s byl odpojen]]>Chyba otevření prohlížečeodstraněn profilový obrázek
- nastavit novou kontaktní adresu
+ nastavil novou kontaktní adresunastavil nový profilový obrázekUložené zprávyPomalá funkce
@@ -1803,8 +1802,8 @@
\nProsím sdělte jakékoli další problémy vývojářům.NeNEposílejte zprávy přímo, i když váš nebo cílový server nepodporuje soukromé směrování.
- SOUBORY
- SOUKROMÉ SMĚROVÁNÍ ZPRÁV
+ Soubory
+ Soukromé směrování zprávTéma profiluPřijata odpověďObnovit barvu
@@ -1878,7 +1877,7 @@
Připomenout pozdějiZkontrolovat aktualizaceVypnuto
- CHAT DATABÁZE
+ Chat databázevypnutinfo fronty serveru: %1$s\n\nposlední obdržená zpráva: %2$sUložit a připojit znovu
@@ -2247,9 +2246,9 @@
NahránoAnoTuto akci nelze zrušit - zprávy odeslané a přijaté v tomto chatu dříve než vybraná, budou smazány.
- Statistiky serverů budou obnoveny - nemůže být vráceno!
+ Statistiky serverů budou obnoveny - nelze vrátit!Odešlete soukromý report
- Pomozte administrátorům moderovat své skupiny.
+ Pomozte správcům moderovat jejich skupiny.Rychlejší mazání skupin.Od %s.Můžete zmínit až %1$s členů ve zprávě!
@@ -2362,7 +2361,7 @@
Členové budou odstraněny ze skupiny - toto nelze zvrátit!Odebrat členy?Členové budou odstraněny z chatu - toto nelze zvrátit!
- Použitím SimpleX chatu souhlasíte že:\n- ve veřejných skupinách budete zasílat pouze legální obsah.\n- budete respektovat ostatní uživatele – žádný spam.
+ Použitím SimpleX Chatu souhlasíte že:\n- ve veřejných skupinách budete zasílat pouze legální obsah.\n- budete respektovat ostatní uživatele – žádný spam.PřijmoutZásady ochrany soukromí a podmínky používání.Soukromé konverzace, skupiny a kontakty nejsou přístupné provozovatelům serverů.
@@ -2430,7 +2429,7 @@
Otevřít novou skupinuPřipojitPřipojte se rychleji! 🚀
- POŽADAVKY NA PŘIPOJENÍ ZE SKUPIN
+ Požadavky na připojení ze skupinkontakt by měl přijmout…Vytvořit vaši adresuPopis příliš dlouhý
@@ -2532,7 +2531,7 @@
FiltrObrázkyOdkazy
- Zprávy člena budou smazány - nemůže být zrušeno!
+ Zprávy člena budou smazány - nelze zrušit!bez předplatnéhoOdebrat a smazat zprávyHledat soubory
@@ -2556,4 +2555,219 @@
Nejstarší lidská svoboda - mluvit s druhým člověkem, aniž by byl sledován - postavena na infrastruktuře, která ji nemůže zradit.Protože jsme zničili sílu vědět, kdo jste. Aby vám vaši moc nikdo nemohl vzít.Buďte svobodní ve své síti.
+ %1$d/%2$d relé aktivních
+ %1$d/%2$d relé aktivních, %3$d chyb
+ %1$d/%2$d relé aktivních, %3$d selhalo
+ %1$d/%2$d relé aktivních, %3$d odstraněno
+ %1$d/%2$d relé připojeno
+ %1$d/%2$d relé připojeno, %3$d chyb
+ %1$d/%2$d relé připojeno, %3$d selhalo
+ %1$d/%2$d relé připojeno, %3$d odstraněno
+ %1$d relé selhalo
+ %1$d relé neaktivních
+ %1$d relé odstraněno
+ %1$d odběratel
+ %1$d odběratelů
+ přijaté
+ Přidávání relé bude podporováno později.
+ Odkaz pro připojení jedné osoby
+ Povolit členům chat se správci.
+ Kanál
+ Kanál
+ Celý název kanálu:
+ Kanál nemá aktivní relé.\nProsím, zkuste se připojit později.
+ Odkaz kanálu
+ Odkaz kanálu
+ Členové kanálu
+ Název kanálu
+ Vlastnosti kanálu
+ kanál
+ Profil Kanálu je uložen na zařízeních odběratelů a na chat relé.
+ profil kanálu aktualizován
+ Kanály
+ Kanál dočasně nedostupný
+ Kanál bude smazán pro všechny odběratele - nelze zrušit!
+ Kanál bude pro vás odstraněn - nelze zvrátit!
+ Kanál začne pracovat s %1$d z %2$d relé. Pokračovat?
+ Chat relé
+ Chat relé
+ Chat relé
+ Chat relé
+ Chat relé předávají zprávy v kanálech, které vytvoříte.
+ Chat relé předávají zprávy na kanál odběratelů.
+ Chat se správci zakázán.
+ Chat se správci ve veřejných kanálech nemají šifrování E2E - používejte pouze s důvěryhodnými chat relé.
+ Chat se členy je zakázán
+ Chat se správci
+ Zkontrolujte relé adresu a zkuste to znovu.
+ Zkontrolujte jméno relé a zkuste to znovu.
+ Nastavit relé
+ Připojit
+ připojeno
+ připojuji
+ Připojení přes odkaz nebo QR kód
+ Vytvořit veřejný kanál
+ Vytvořit veřejný kanál
+ Vytvořit veřejný kanál (BETA)
+ Vytvořte odkaz
+ Vytvořte si veřejnou adresu
+ Vytvářím kanál
+ %d událostí kanálu
+ Dekódovací odkaz
+ Smazat kanál
+ Smazat kanál?
+ smazán
+ smazaný kanál
+ Smazat relé
+ Přímé zprávy mezi odběrateli jsou zakázány.
+ Zakázat
+ Neposílat historii novým odběratelům.
+ Snadněji pozvěte své přátele 👋
+ Upravit profil kanálu
+ Povolit
+ Povolit
+ Povolte alespoň jedno chat relé pro vytvoření kanálu.
+ Povolit chat se správci?
+ Povolit náhledy odkazů?
+ Zadejte název profilu…
+ Zadejte jméno relé…
+ Chyba
+ Chyba přidávání relé
+ Chyba vytváření kanálu
+ Chyba otevírání kanálu
+ chyba: %s
+ Chyba ukládání profilu kanálu
+ Chyba sdílení kanálu
+ selhalo
+ selhalo
+ Pro spojení s kýmkoli
+ (od majitele)
+ Získat odkaz
+ Začít
+ Odkaz skupiny
+ Historie není odesílána novým odběratelům.
+ neaktivní
+ Špatná relé adresa!
+ Neplatné jméno relé!
+ pozván
+ Pozvat soukromě
+ Připojit ke kanálu
+ Opustit kanál
+ Opustit kanál?
+ Přidat
+ Přidat relé
+ Přidat relé
+ Povolit odběratelům odesílání přímých zpráv.
+ Povolit odběratelům chat se správci.
+ Všechna relé selhala
+ Všechny relé odebrány
+ Buďte volní\nve vaší síti
+ Blokovat uživatele všem?
+ Spodní lišta
+ Vysílání
+ Test relé pro načtení jeho jména.]]>
+ Obchodní adresa
+ Zrušit a odstranit kanál
+ Zrušit vytvoření kanálu?
+ nemůže vysílat
+ Zavřít aplikaci
+ Adresa kontaktu
+ %d relé vybráno
+ zahozeno (%1$d pokusů)
+ Chyba přidávání relé
+ Chyba odstranění zprávy
+ Z historie
+ Pokud vyberete Zavřít, zprávy nebudou přijímány.\nMůžete to změnit později v nastavení Vzhledu.
+ Nechat SimpleX běžet na pozadí pro přijímání zpráv.
+ Umožněte ostatním se s vámi spojit
+ Odkaz
+ Náhled odkazu bude vyžádán prostřednictvím SOCKS proxy. DNS vyhledávání se stále může uskutečnit lokálně pomocí vašeho DNS resolveru.
+ Podpis odkazu ověřen.
+ Členové mohou chatovat se správci.
+ Chyba zprávy
+ end-to-end nešifrované. Chat relé může vidět tyto zprávy.]]>
+ Přenést
+ Skrýt do oznamovací oblasti
+ Skrýt do oznamovací oblasti?
+ Skrýt do oznamovací oblasti při zavření okna
+ Chyba sítě
+ nové
+ Nový jednorázový odkaz
+ Nové chat relé
+ Žádné aktivní relé
+ Žádné dostupné relé
+ Žádné chat relé
+ Není povoleno žádné relé.
+ Žádné relé
+ Relé nevybráno
+ Ne všechny relé připojeny
+ Jednorázový odkaz
+ Pouze majitelé kanálu mohou měnit nastavení.
+ Na vašem telefonu, ne na serverech.
+ Otevřít kanál
+ Otevřít externí odkaz?
+ Otevřít nový kanál
+ Nebo ukažte QR osobně nebo prostřednictvím videohovoru.
+ Nebo použijte tento QR kód - tisk nebo zobrazit online.
+ MAJITEL
+ Majitelé
+ Vlastnictví: můžete provozovat vlastní relé.
+ Soukromí: pro majitele a odběratele.
+ Soukromé a bezpečné zasílání zpráv.
+ Zakázat chat se správci.
+ Zakázat odesílání přímých zpráv odběratelům.
+ Veřejné kanály - mluvte volně 🚀
+ Opustit SimpleX
+ relé
+ RELÉ
+ Relé adresa
+ Relé adresa
+ Připojení relé selhalo
+ Relé odkaz
+ Relé výsledky:
+ Relé přidáno: %1$s.
+ Relé test selhal!
+ odstraněno
+ odstraněno operátorem
+ Odstranit relé
+ Odstranit relé?
+ Odstranit odběratele
+ Odstranit odběratele?
+ Bezpečné odkazy
+ Uložit a informovat odběratele kanálu
+ Uložit profil kanálu
+ Vybrat relé
+ Varování serveru
+ Nastavit oznamování
+ Nastavit routery
+ Sdílet kanál…
+ Sdílet adresu relé
+ Sdílet pomocí chatu
+ Zobrazit SimpleX
+ ⚠️ Ověření podpisu selhalo: %s.
+ (podepsán)
+ SimpleX
+ SimpleX - %d nepřečteno
+ ODBĚRATEL
+ Hlášení odběratelů
+ Odběratelé
+ Odběratelé mohou přidávat reakce na zprávy.
+ Odběratelé mohou psát správcům.
+ Odběratelé mohou nevratně mazat odeslané zprávy. (24 hodin)
+ Odběratelé mohou nahlásit zprávy moderátorům.
+ Odběratelé mohou posílat přímé zprávy.
+ Odběratelé mohou odesílat mizející zprávy.
+ Odběratelé mohou posílat soubory a média.
+ Odběratelé mohou odesílat SimpleX odkazy.
+ Odběratelé mohou posílat hlasové zprávy.
+ Odběratelé použijí relé odkaz pro připojení ke kanálu.\nRelé adresa byla použita pro nastavení tohoto kanálu.
+ Odběratelé budou odebráni z kanálu - Nelze zvrátit!
+ Promluvte si s někým
+ Klepněte na Přidat kanál
+ Klepněte pro otevření
+ Test selhal v kroku %s.
+ Test relé
+ Aplikace odstranila tuto zprávu po %1$d pokusech o přijetí.
+ Připojení dosáhlo limitu nedoručených zpráv
+ První síť, kde vy vlastníte\nvaše kontakty a skupiny.
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/da/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/da/strings.xml
index 38507cc228..9487b85cb3 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/da/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/da/strings.xml
@@ -157,7 +157,7 @@
En anden grundSvaropkaldAlle kan være vært for servere.
- APP
+ AppApp løber altid i baggrundenApp Build: %sApp kan kun modtage meddelelser, når den kører, ingen baggrundstjeneste startes
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml
index 8700ade74e..c8bb00bfa9 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml
@@ -404,7 +404,7 @@
Onion-Hosts werden nicht verwendet.Für die Verbindung werden Onion-Hosts benötigt.
\nBitte beachten Sie: Ohne .onion-Adresse können Sie keine Verbindung mit den Servern herstellen.
- Erscheinungsbild
+ DarstellungAdresse erstellenAdresse löschen?
@@ -495,14 +495,14 @@
Audio- & VideoanrufeIhre Anrufe
- Immer über ein Relais verbinden
+ Immer über einen Router verbindenAnrufe auf Sperrbildschirm:AkzeptierenAnzeigenDeaktivierenIhre ICE-ServerWebRTC-ICE-Server
- Relais-Server schützen Ihre IP-Adresse, aber sie können die Anrufdauer erfassen.
+ Relais-Server schützen Ihre IP-Adresse, können aber die Anrufdauer erfassen.Relais-Server werden nur genutzt, wenn sie benötigt werden. Ihre IP-Adresse kann von Anderen erfasst werden.Öffnen Sie SimpleX Chat, um den Anruf anzunehmen.
@@ -549,26 +549,26 @@
Linkvorschau sendenApp-Datensicherung
- MEINE DATEN
- EINSTELLUNGEN
- HILFE
- UNTERSTÜTZUNG VON SIMPLEX CHAT
- GERÄT
- CHATS
+ Meine Daten
+ Einstellungen
+ Hilfe
+ Unterstützung von SimpleX Chat
+ Gerät
+ ChatsEntwicklertoolsExperimentelle Funktionen
- SOCKS-PROXY
- APP-ICON
- DESIGN
- NACHRICHTEN und DATEIEN
- CALLS
+ SOCKS-Proxy
+ App-Icon
+ Design
+ Nachrichten und Dateien
+ CallsInkognito-ModusChat-Datenbank
- CHAT STARTEN
+ Chat startenDer Chat läuftDer Chat ist beendet
- CHAT-DATENBANK
+ Chat-DatenbankDatenbank-PasswortDatenbank exportierenDatenbank importieren
@@ -747,7 +747,7 @@
Sie versuchen, einen Kontakt, mit dem Sie ein Inkognito-Profil geteilt haben, in die Gruppe einzuladen, in der Sie Ihr Hauptprofil verwenden.Mitglieder einladen
- %1$s MITGLIEDER
+ %1$s MitgliederSie: %1$sGruppe löschenGruppe löschen?
@@ -765,7 +765,7 @@
Fehler beim Löschen des Gruppen-LinksGruppen-Präferenzen können nur von Gruppen-Eigentümern geändert werden.
- FÜR KONSOLE
+ Für KonsoleLokaler NameDatenbank-ID
@@ -773,7 +773,7 @@
Direktnachricht sendenDas Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden!Entfernen
- MITGLIED
+ MitgliedRolleRolle ändernÄndern
@@ -788,7 +788,7 @@
direktindirekt (%1$s)
- SERVER
+ ServerEmpfangen überSenden überNetzwerkstatus
@@ -1065,7 +1065,7 @@
Datenbank-Aktualisierungen bestätigenAnzeigen:Entwickleroptionen anzeigen
- EXPERIMENTELL
+ ExperimentellDatenbank-AktualisierungUnterschiedlicher Migrationsstand in der App/Datenbank: %s / %sDatenbank herabstufen und den Chat öffnen
@@ -1189,7 +1189,7 @@
Wenn Personen eine Verbindung anfordern, können Sie diese annehmen oder ablehnen.Sie werden Ihre damit verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.Design anpassen
- INTERFACE-FARBEN
+ Interface-FarbenFügen Sie die Adresse Ihrem Profil hinzu, damit Ihre SimpleX-Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre SimpleX-Kontakte gesendet.Alle Ihre Kontakte bleiben verbunden. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet.Erstellen Sie eine Adresse, damit sich Personen mit Ihnen verbinden können.
@@ -1309,7 +1309,7 @@
Während des Imports sind nicht schwerwiegende Fehler aufgetreten:Herunterfahren\?Bis zum Neustart der App erhalten Sie keine Benachrichtigungen mehr
- APP
+ AppNeustartHerunterfahrenFehler beim Beenden des Adresswechsels
@@ -1372,7 +1372,7 @@
Bestätigungen aktivieren\?Das Senden von Bestätigungen an %d Kontakte ist aktiviertFür alle aktivieren
- EMPFANGSBESTÄTIGUNGEN SENDEN AN
+ Empfangsbestätigungen senden anDeaktivieren (vorgenommene Einstellungen bleiben erhalten)Bestätigungen sendenIhre Verbindungen beibehalten
@@ -1437,7 +1437,7 @@
Datenbank-Ordner öffnenDas Passwort wird in Klartext in den Einstellungen gespeichert, nachdem Sie es geändert oder die App neu gestartet haben.Das Passwort wurde in Klartext in den Einstellungen gespeichert.
- Bitte beachten Sie: Die Nachrichten- und Datei-Relais sind per SOCKS-Proxy verbunden. Anrufe nutzen eine direkte Verbindung.]]>
+ Bitte beachten Sie: Die Nachrichten- und Datei-Router sind per SOCKS-Proxy verbunden. Anrufe nutzen eine direkte Verbindung.]]>Lokale Dateien verschlüsselnÖffnenGespeicherte Dateien & Medien verschlüsseln
@@ -1702,7 +1702,7 @@
Link-Details werden heruntergeladenArchiv wird heruntergeladenAnwenden
- Alle Ihre Kontakte, Unterhaltungen und Dateien werden sicher verschlüsselt und in Daten-Paketen auf die konfigurierten XTFP-Relais hochgeladen.
+ Alle Ihre Kontakte, Unterhaltungen und Dateien werden sicher verschlüsselt und in Daten-Paketen auf die konfigurierten XTFP-Router hochgeladen.Archivieren und HochladenWarnung: Das Archiv wird gelöscht.]]>Überprüfen Sie Ihre Internetverbindung und probieren Sie es nochmals
@@ -1840,8 +1840,8 @@
NieUnbekannte ServerUngeschützt
- Sie nutzen privates Routing mit unbekannten Servern.
- Sie nutzen KEIN privates Routing.
+ Bei unbekannten Servern privates Routing nutzen.
+ KEIN privates Routing nutzen.Modus für das Nachrichten-RoutingJaNein
@@ -1849,20 +1849,19 @@
Fallback für das Nachrichten-RoutingNachrichtenstatus anzeigenHerabstufung erlauben
- Sie nutzen immer privates Routing.
+ Immer privates Routing nutzen.Nachrichten werden nicht direkt versendet, selbst wenn Ihr oder der Ziel-Server kein privates Routing unterstützt.
- PRIVATES NACHRICHTEN-ROUTING
+ Privates Nachrichten-RoutingNachrichten werden direkt versendet, wenn die IP-Adresse geschützt ist, und Ihr oder der Ziel-Server kein privates Routing unterstützt.Nachrichten werden direkt versendet, wenn Ihr oder der Ziel-Server kein privates Routing unterstützt.
- Zum Schutz Ihrer IP-Adresse, wird für die Nachrichten-Auslieferung privates Routing über Ihre konfigurierten SMP-Server genutzt.
- Sie nutzen privates Routing mit unbekannten Servern, wenn Ihre IP-Adresse nicht geschützt ist.
+ Zum Schutz Ihrer IP-Adresse, wird für die Nachrichten-Auslieferung privates Routing über Ihre konfigurierten SMP-Router genutzt.
+ Bei unbekannten Servern privates Routing nutzen, wenn Ihre IP-Adresse nicht geschützt ist.IP-Adresse schützen
- DATEIEN
+ DateienDie App wird bei unbekannten Datei-Servern nach einer Download-Bestätigung fragen (außer bei .onion oder wenn ein SOCKS-Proxy aktiviert ist).Unbekannte Server!Ohne Tor- oder VPN-Nutzung wird Ihre IP-Adresse für Datei-Server sichtbar sein.
- Ohne Tor- oder VPN-Nutzung wird Ihre IP-Adresse für diese XFTP-Relais sichtbar sein:
-\n%1$s.
+ Ohne Tor- oder VPN-Nutzung wird Ihre IP-Adresse für diese XFTP-Router sichtbar sein: \n%1$s.Profil-DesignSchwarzFarbvariante
@@ -1903,8 +1902,7 @@
Gestalten Sie Ihre Chats unterschiedlich!Neue Chat-DesignsPrivates Nachrichten-Routing 🚀
- Schützen Sie Ihre IP-Adresse vor den Nachrichten-Relais , die Ihr Kontakt ausgewählt hat.
-\nAktivieren Sie es in den *Netzwerk & Server* Einstellungen.
+ Schützen Sie Ihre IP-Adresse vor den Nachrichten-Routern, die Ihre Kontakte ausgewählt haben.\nAktivieren Sie es in den *Netzwerk & Server* Einstellungen.Dateien sicher herunterladenMit reduziertem Akkuverbrauch.Keine Information
@@ -2107,7 +2105,7 @@
ErstellenLaden Sie neue Versionen von GitHub herunter.Direkt aus der Chat-Liste abspielen.
- Kann von Ihnen in den Erscheinungsbild-Einstellungen geändert werden.
+ Sie können dies in den Einstellungen unter „Darstellung“ ändern.Kontakte für spätere Chats archivieren.Ihre IP-Adresse und Verbindungen werden geschützt.Löschen Sie bis zu 20 Nachrichten auf einmal.
@@ -2126,7 +2124,7 @@
Neue NachrichtBitte überprüfen Sie, ob der SimpleX-Link korrekt ist.Ungültiger Link
- CHAT-DATENBANK
+ Chat-DatenbankFehler beim Wechseln des ProfilsDie Nachrichten werden gelöscht. Dies kann nicht rückgängig gemacht werden!Profil teilen
@@ -2135,15 +2133,15 @@
Chat-Profil auswählenArchiv entfernen?Ihre Anmeldeinformationen können unverschlüsselt versendet werden.
- Verwenden Sie keine Anmeldeinformationen mit einem Proxy.
+ Keine Anmeldeinformationen mit einem Proxy verwenden.Ihre Verbindung wurde auf %s verschoben, aber während des Profil-Wechsels trat ein Fehler auf.Stellen Sie sicher, dass die Proxy-Konfiguration richtig ist.Fehler beim Speichern des ProxysPasswortProxy-Authentifizierung
- Verwenden Sie für jede Verbindung unterschiedliche Proxy-Anmeldeinformationen.
- Verwenden Sie für jedes Profil unterschiedliche Proxy-Anmeldeinformationen.
- Verwenden Sie zufällige Anmeldeinformationen
+ Für jede Verbindung unterschiedliche Proxy-Anmeldeinformationen verwenden.
+ Für jedes Profil unterschiedliche Proxy-Anmeldeinformationen verwenden.
+ Zufällige Anmeldeinformationen verwendenBenutzername%1$d Datei-Fehler:\n%2$s%1$d Datei(en) wird/werden immer noch heruntergeladen.
@@ -2179,7 +2177,7 @@
Die SimpleX-Protokolle wurden von Trail of Bits überprüft.Während des Anrufs zwischen Audio und Video wechselnDas Chat-Profil für Einmal-Einladungen wechseln
- Jedes Mal wenn Sie die App starten, werden neue SOCKS-Anmeldeinformationen genutzt
+ Bei jedem Neustart der App, werden neue SOCKS-Anmeldeinformationen genutztVerbesserte Sicherheit ✅Verbesserte Nachrichten-DatumsinformationVerbesserte Nutzer-Erfahrung
@@ -2215,7 +2213,7 @@
AktualisierenVoreingestellte ServerNutzungsbedingungen einsehen
- Die Nutzungsbedingungen der aktivierten Betreiber werden automatisch akzeptiert am: %s.
+ Die Nutzungsbedingungen der aktivierten Betreiber werden am %s automatisch akzeptiert.Ihre ServerBetreiber%s Server
@@ -2258,7 +2256,7 @@
nur mit einem Kontakt genutzt werden - teilen Sie ihn nur persönlich oder über einen beliebigen Messenger.]]>%s.]]>%s.]]>
- Die Nutzungsbedingungen wurden akzeptiert am: %s
+ Die Nutzungsbedingungen wurden am %s akzeptiert.%s.]]>%s zu nutzen, müssen Sie dessen Nutzungsbedingungen akzeptieren.]]>Fehler beim Akzeptieren der Nutzungsbedingungen
@@ -2274,7 +2272,7 @@
Zum Schutz vor dem Austausch Ihres Links können Sie die Sicherheitscodes Ihrer Kontakte vergleichen.%s.]]>%s.]]>
- Die Nutzungsbedingungen wurden akzeptiert am: %s.
+ Die Nutzungsbedingungen wurden am %s akzeptiert.Der Text der aktuellen Nutzungsbedingungen konnte nicht geladen werden. Sie können die Nutzungsbedingungen unter diesem Link einsehen:Ferngesteuerte MobiltelefoneOder importieren Sie eine Archiv-Datei
@@ -2380,7 +2378,7 @@
%d MeldungenMitglieder-MeldungenMeldungen
- Inhalt verletzt Nutzungsbedingungen
+ Inhalt verletzt die NutzungsbedingungenSpamVerbindung blockiertDie Datei wird vom Serverbetreiber blockiert:\n%1$s.
@@ -2583,7 +2581,7 @@
Alten Link teilenDer Link wird gekürzt sein, und das Gruppen-Profil wird über den Link geteilt.Gruppen-Link aktualisieren
- KONTAKTANFRAGEN VON GRUPPEN
+ Kontaktanfragen von GruppenMitglied ist gelöscht - Anfrage kann nicht angenommen werdenAngefragte Verbindung von Gruppe %1$sDiese Einstellung gilt für Ihr aktuelles Profil
@@ -2626,7 +2624,7 @@
Sprachnachrichten suchenVideosSprachnachrichten
- VERBINDUNG FEHLGESCHLAGEN
+ Verbindung fehlgeschlagenFehlgeschlagenKanäle, welche Sie erstellt haben oder denen Sie beigetreten sind, werden dauerhaft deaktiviert.%1$d/%2$d Relais aktiv
@@ -2695,12 +2693,12 @@
Es sind nicht alle Relais verbundenKanal öffnenNeuen Kanal öffnen
- EIGENTÜMER
+ EigentümerEigentümerVoreingestellte Relais-AdresseVoreingestellter Relais-NameRelais
- RELAIS
+ RelaisRelais-AdresseRelais-AdresseRelais-Verbindung fehlgeschlagen
@@ -2708,10 +2706,10 @@
Relais-Test fehlgeschlagen!Abonnent entfernenAbonnent entfernen?
- Der Server erfordert eine Autorisierung, um eine Verbindung zum Relais herzustellen. Bitte Passwort überprüfen.
+ Der Server erfordert eine Autorisierung, um eine Verbindung zum Router herzustellen. Bitte Passwort überprüfen.ServerwarnungRelais-Adresse teilen
- ABONNENT
+ AbonnentAbonnentenAbonnenten verbinden sich über den Relais‑Link mit dem Kanal.\nDie Relais-Adresse wurde zur Einrichtung dieses Relais für diesen Kanal verwendet.Abonnent wird aus dem Kanal entfernt. Dies kann nicht rückgängig gemacht werden!
@@ -2757,7 +2755,7 @@
%1$d Relais fehlgeschlagen%1$d Relais nicht aktiv%1$d Relais entfernt
- Das Hinzufügen von Relais wird zu einem späteren Zeitpunkt unterstützt.
+ Relais hinzufügen, um die Nachrichtenübermittlung wiederherzustellen.Alle Relais fehlgeschlagenAlle Relais entferntBroadcast nicht möglich
@@ -2805,11 +2803,11 @@
Oder den QR‑Code persönlich oder per Videoanruf zeigen.Oder diesen QR‑Code verwenden – ausgedruckt oder online.Volle Kontrolle: Sie können Ihre eigenen Relais betreiben.
- Privatsphäre: für Besitzer und Abonnenten.
+ Privatsphäre: Für Eigentümer und Abonnenten.Öffentliche Kanäle – frei sprechen 🚀
- Zuverlässigkeit: mehrere Relais pro Kanal.
+ Zuverlässigkeit: Mehrere Relais pro Kanal.Sichere Web-Links
- Sicherheit: Eigentümer besitzen die Kanalschlüssel.
+ Sicherheit: Nur die Eigentümer des Kanals besitzen die Schlüssel.Den Link über einen beliebigen Messenger versenden – es ist sicher. Bitte in SimpleX einfügen.Mit Jemandem sprechenFür ein dauerhaftes SimpleX-Netzwerk.
@@ -2873,4 +2871,38 @@
Untere LeisteDie Linkvorschau wird über einen SOCKS-Proxy angefordert. DNS-Abfragen können dennoch lokal über Ihren DNS-Resolver erfolgen.Obere Leiste
+ Ihr neuer Kanal %1$s ist mit %2$d von %3$d Relais verbunden.\nBei Abbruch, wird der Kanal gelöscht. Sie können ihn später neu erstellen.
+ Hinzufügen
+ Relais hinzufügen
+ Relais hinzufügen
+ Kanal abbrechen und löschen
+ %d Relais ausgewählt
+ Fehler beim Hinzufügen von Relais
+ Keine verfügbaren Relais
+ Keine Relais
+ Keine Relais ausgewählt
+ %1$s Relais hinzugefügt.
+ Relais wird aus dem Kanal entfernt. Dies kann nicht rückgängig gemacht werden!
+ Entfernt
+ Relais entfernen
+ Relais entfernen?
+ Relais auswählen
+ Dies ist das letzte aktive Relais. Wenn Sie es entfernen, können keine Nachrichten mehr an Abonnenten zugestellt werden.
+ App schließen
+ Wenn Sie \"Schließen\" auswählen, werden keine Nachrichten mehr empfangen.\nSie können dies später in den Einstellungen unter \"Darstellung\" ändern.
+ SimpleX im Hintergrund weiter ausführen, um Nachrichten zu empfangen.
+ In den Infobereich minimieren
+ In den Infobereich minimieren?
+ Beim Schließen des Fensters in den Infobereich minimieren
+ SimpleX beenden
+ SimpleX anzeigen
+ SimpleX
+ SimpleX — %d ungelesen
+ Fehler beim Löschen der Nachricht
+ Aus dem Nachrichtenverlauf
+ App ist bereits gestartet
+ App ist eventuell schon gestartet oder wurde nicht richtig beendet. Trotzdem starten?
+ Abgelehnt
+ Status
+ Vom Relais-Betreiber abgelehnt
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml
index 47cfd90ad6..390913c5f7 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml
@@ -86,7 +86,7 @@
Ο ΙCE διακομιστής σουΚωδικός πρόσβασης εφαρμογήςΑίτημα σύνδεσης θα σταλεί σε αυτό το μέλος της ομάδας.
- ΟΙΚΟΝΑ ΕΦΑΡΜΟΓΗΣ
+ Εικόνα εφαρμογήςΕφαρμογήΟι ρυθμίσεις σουΈκδοση εφαρμογής: v%s
@@ -105,7 +105,7 @@
Άλλαξε\nΔιαθέσιμο στην έκδοση 5.1Τέλος κλήσης
- ΚΛΗΣΕΙΣ
+ ΚλήσειςΑυτόματη αποδοχή%1$d αποτυχία κρυπτογράφησης μηνύματοςαλλαγή διεύθυνσης για %s…
@@ -252,7 +252,7 @@
Eνεργοποίηση ήχουΚακό μήνυμα hashΘάμπωση των μέσων
- ΒΑΣΗ ΔΕΔΟΜΕΝΩΝ ΣΥΝΟΜΙΛΙΑΣ
+ Βάση δεδομένων συνομιλίαςΤο Android Keystore χρησιμοποιείται για την ασφαλή αποθήκευση της φράσης πρόσβασης - επιτρέπει την υπηρεσία ειδοποιήσεων να λειτουργεί.αποκλεισμένοςΑποκλεισμένος από τον διαχειριστή
@@ -288,7 +288,7 @@
Αρχειοθετημένες επαφέςΑκύρωση μεταφοράςΧρώματα συνομιλίας
- ΒΑΣΗ ΔΕΔΟΜΕΝΩΝ ΣΥΝΟΜΙΛΙΑΣ
+ Βάση δεδομένων συνομιλίαςΗ συνομιλία εκτελείταιΠαρακαλώ σημείωσε: ΔΕΝ θα μπορείς να ανακτήσεις ή να αλλάξεις τη φράση πρόσβασης εάν τη χάσεις.]]>Αποκλεισμός για όλους
@@ -377,7 +377,7 @@
κακό αναγνωριστικό μηνύματοςΑπάντηση κλήσηςΚακό αναγνωριστικό μηνύματος
- ΣΥΝΟΜΙΛΙΕΣ
+ ΣυνομιλίεςΗ βάση δεδεδομένων της συνομιλίας εισάχθηκεσυμφωνία κρυπτογράφησης για %s…Να επιτραπούν οι κλήσεις;
@@ -600,7 +600,7 @@
Κρυμμένη επαφή:Η επαφή διαγράφηκε.η επαφή δεν είναι έτοιμη
- ΑΙΤΗΣΕΙΣ ΕΠΑΦΩΝ ΑΠΟ ΟΜΑΔΕΣ
+ Αιτήσεις επαφών από ομάδεςΕπαφέςη επαφή πρέπει να αποδεχτεί…Η επαφή θα διαγραφεί – αυτή η ενέργεια δεν μπορεί να αναιρεθεί!
@@ -759,7 +759,7 @@
ΛεπτομέρειεςΕπιλογές προγραμματιστήΕργαλεία προγραμματιστή
- ΣΥΣΚΕΥΗ
+ ΣυσκευήΗ επαλήθευση συσκευής είναι απενεργοποιημένη. Απενεργοποιείται το SimpleX Lock.Η επαλήθευση συσκευής δεν είναι ενεργοποιημένη. Μπορείς να ενεργοποιήσεις το SimpleX Lock από τις Ρυθμίσεις, αφού πρώτα ενεργοποιήσεις την επαλήθευση συσκευής.Συσκευές
@@ -927,7 +927,7 @@
Για όλους τους διαχειριστέςγια καλύτερη ιδιωτικότητα μεταδεδομένωνΓια το προφίλ συνομιλίας %s:
- ΓΙΑ ΚΟΝΣΟΛΑ
+ Για κονσόλαΓια όλουςΓια παράδειγμα, αν η επαφή σου λαμβάνει μηνύματα μέσω κάποιου SimpleX Chat διακομιιστή, η εφαρμογή σου θα τα παραδίδει μέσω ενός Flux διακομιστή.Για μένα
@@ -986,7 +986,7 @@
Τερματισμός κλήσηςΑκουστικάβοήθεια
- ΒΟΗΘΕΙΑ
+ ΒοήθειαΒοήθησε τους διαχειριστές να διαχειρίζονται τις ομάδες τους.Γεια σου!\nΣυνδέσου μαζί μου μέσω SimpleX Chat: %sΚρυφό
@@ -1069,7 +1069,7 @@
Άμεσες ειδοποιήσειςΆμεσες ειδοποιήσεις!Οι άμεσες ειδοποιήσεις είναι απενεργοποιημένες!
- ΧΡΩΜΑΤΑ ΔΙΕΠΑΦΗΣ
+ Χρώματα διεπαφήςΕσωτερικό σφάλμαμη έγκυρη συνομιλίαΜη έγκυρος σύνδεσμος
@@ -1175,7 +1175,7 @@
Διακομιστές πολυμέσων & αρχείωνΜεσαίομέλος
- ΜΕΛΟΣ
+ ΜέλοςΜέλος %1$sτο μέλος %1$s άλλαξε σε %2$sΕγγραφή μέλους
@@ -1219,7 +1219,7 @@
Εναλλακτική δρομολόγηση μηνυμάτωνΛειτουργία δρομολόγησης μηνυμάτωνΜηνύματα
- ΜΗΝΥΜΑΤΑ ΚΑΙ ΑΡΧΕΙΑ
+ Μηνύματα και αρχείαΔιακομιστές μηνυμάτωνΘα εμφανιστούν τα μηνύματα από το %s!Θα εμφανιστούν τα μηνύματα από αυτά τα μέλη!
@@ -1332,7 +1332,7 @@
Η εικόνα δεν μπορεί να αποκωδικοποιηθεί. Δοκίμασε μια άλλη εικόνα ή επικοινώνησε με τους προγραμματιστές.Ο σύνδεσμος θα είναι σύντομος και το προφίλ της ομάδας θα κοινοποιηθεί μέσω αυτού.Θέμα
- ΘΕΜΑΤΑ
+ ΘέματαΤα μηνύματα θα διαγραφούν για όλα τα μέλη.Τα μηνύματα θα επισημαίνονται ως ελεγχόμενα για όλα τα μέλη.Το μήνυμα θα διαγραφεί για όλα τα μέλη.
@@ -1582,7 +1582,7 @@
Έξοδος χωρίς αποθήκευσηΕπέκτεινεΕπέκταση επιλογής ρόλου
- ΠΕΙΡΑΜΑΤΙΚΟ
+ ΠειραματικόΠειραματικά χαρακτηριστικάέληξεΕξαγωγή της βάσης δεδομένων
@@ -1604,7 +1604,7 @@
Το αρχείο δεν βρέθηκε - πιθανότατα το αρχείο διαγράφηκε ή ακυρώθηκε.Αρχείο: %sΑρχεία
- ΑΡΧΕΙΑ
+ ΑρχείαΑρχεία και πολυμέσαΑπαγορεύονται τα αρχεία και τα πολυμέσα.Τα αρχεία και τα πολυμέσα, απαγορεύονται σε αυτήν τη συνομιλία.
@@ -1863,7 +1863,7 @@
Ιδιωτικά ονόματα αρχείωνΙδιωτικά ονόματα αρχείων πολυμέσων.Δρομολόγηση ιδιωτικών μηνυμάτων 🚀
- ΔΡΟΜΟΛΟΓΗΣΗ ΙΔΙΩΤΙΚΩΝ ΜΗΝΥΜΑΤΩΝ
+ Δρομολόγηση ιδιωτικών μηνυμάτωνΙδιωτικές σημειώσειςΙδιωτικές σημειώσειςΙδιωτικές ειδοποιήσεις
@@ -2032,7 +2032,7 @@
Ανάκληση αρχείουΑνάκληση αρχείου;Ρόλος
- ΕΚΚΙΝΗΣΗ ΣΥΝΟΜΙΛΙΑΣ
+ Εκκίνηση συνομιλίαςΕκτελείται όταν η εφαρμογή είναι ανοιχτήΑσφαλής λήψη αρχείωνΑσφαλέστερες ομάδες
@@ -2098,7 +2098,7 @@
ΑπέστειλεΣτείλε ένα ζωντανό μήνυμα - θα ενημερώνεται για τον παραλήπτη ή τους παραλήπτες καθώς το πληκτρολογείς.Αποστολή αιτήματος επαφής;
- ΑΠΟΣΤΟΛΗ ΑΝΑΦΟΡΩΝ ΠΑΡΑΔΟΣΗΣ ΣΕ
+ Αποστολή αναφορών παράδοσης σεΑποστολή άμεσου μηνύματοςΣτείλε άμεσο μήνυμα για να συνδεθείςΑποστολή μηνύματος που εξαφανίζεται
@@ -2153,7 +2153,7 @@
πληροφορίες ουράς διακομιστή: %1$s\n\nτελευταίο ληφθέν μήνυμα: %2$sΟ διακομιστής απαιτεί εξουσιοδότηση για τη δημιουργία ουρών, έλεγξε τον κωδικό.Ο διακομιστής απαιτεί εξουσιοδότηση για ανέβασμα αρχείων, έλεγξε τον κωδικό.
- ΔΙΑΚΟΜΙΣΤΕΣ
+ ΔιακομιστέςΠληροφορίες διακομιστώνΘα γίνει επαναφορά στα στατιστικά στοιχεία των διακομιστών - αυτή η ενέργεια δεν μπορεί να αναιρεθεί!Η δοκιμή του διακομιστή απέτυχε!
@@ -2179,7 +2179,7 @@
Όρισε το εμφανιζόμενο μήνυμα για τα νέα μέλη!ΡυθμίσειςΡυθμίσεις
- ΡΥΘΜΙΣΕΙΣ
+ ΡυθμίσειςΌρισε τη φράση πρόσβασης της βάσης δεδομένωνΔιαμόρφωση εικόνων προφίλΔιαμοίρασε
@@ -2260,7 +2260,7 @@
Διακομιστής SMPΔιακομιστές SMPΔιακομιστής μεσολάβησης SOCKS
- ΔΙΑΚΟΜΙΣΤΗΣ ΜΕΣΟΛΑΒΗΣΗΣ SOCKS
+ Διακομιστής μεσολάβησης SOCKSΡυθμίσεις διακομιστή μεσολάβησης SOCKSΑπαλόΚάποιο/α αρχείο/α δεν εξήχθησαν
@@ -2309,7 +2309,7 @@
Η εγγραφή αγνοήθηκε%s ανεβασμέναΥποστήριξη bluetooth και άλλων βελτιώσεων.
- ΥΠΟΣΤΗΡΙΞΗ SIMPLEX CHAT
+ Υποστήριξη SimpleX ChatΕνάλλαξεΕναλλαγή ήχου και βίντεο κατά τη διάρκεια της κλήσης.Αλλαγή προφίλ συνομιλίας για προσκλήσεις 1-χρήσης.
@@ -2414,7 +2414,7 @@
ΝαιΝαιεσύ
- ΕΣΥ
+ Εσύεσύ: %1$sΑποδέχθηκες τη σύνδεσηαποδέχθηκες αυτό το μέλος
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml
index 7088c54d9b..cdb8a71d7b 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml
@@ -20,7 +20,7 @@
Permites que tus contactos envien mensajes de voz.siempreLa aplicación sólo puede recibir notificaciones cuando se está ejecutando. No se iniciará ningún servicio en segundo plano.
- ICONO DE LA APLICACIÓN
+ Icono de la aplicaciónLa optimización de la batería está activa, desactivando el servicio en segundo plano y las solicitudes periódicas de nuevos mensajes. Puedes volver a activarlos en Configuración.El servicio está siempre en funcionamiento en segundo plano. Las notificaciones se muestran en cuanto haya mensajes nuevos.Se puede desactivar en la configuración. En ese caso las notificaciones se seguirán mostrando mientras la aplicación esté en funcionamiento.]]>
@@ -200,7 +200,7 @@
Eliminar servidorIntroduce tu nombre:conectado
- DISPOSITIVO
+ DispositivoContraseña base de datosEliminar base de datosEliminar todos los archivos
@@ -243,7 +243,7 @@
Core versión: v%sEliminar imagenEditar imagen
- CHATS
+ ChatsCambiarSe realizan comprobaciones de mensajes nuevos periódicas de hasta un minuto de duración cada 10 minutosLimpiar
@@ -274,7 +274,7 @@
Preferencias generalescancelado %sSimpleX está parado
- LLAMADAS
+ LlamadasSimpleX está en ejecuciónestá cambiando de servidor…habla con los desarrolladores
@@ -295,7 +295,7 @@
Llamadas en la ventana de bloqueo¡No se pueden invitar contactos!Consola de Chat
- BASE DE DATOS DE SIMPLEX
+ Base de datos de SimpleXBase de datos eliminadaBase de datos importadaComprueba la dirección del servidor e inténtalo de nuevo.
@@ -393,15 +393,15 @@
Archivo no encontradoGuía de usofinalizado
- AYUDA
+ AyudaExportar base de datosError al exportar base de datosError al iniciar Chatse ha unido mediante tu enlace de grupoError al actualizar enlace de grupo
- PARA CONSOLA
+ Para consolaError al cambiar rol
- SERVIDORES
+ ServidoresNombre del grupo:Preferencias del grupoLos miembros pueden enviar mensajes directos.
@@ -455,7 +455,7 @@
\n2. El descifrado ha fallado porque tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos.
\n3. La conexión ha sido comprometida.Contacto y texto
- MIEMBRO
+ MiembronuncaNo se usarán hosts .onionVista previa de notificaciones
@@ -524,7 +524,7 @@
videollamada (sin cifrar)sin cifrarImportar base de datos
- MENSAJES Y ARCHIVOS
+ Mensajes y archivos¿Importar base de datos\?Sin archivos recibidos o enviadosMensajes
@@ -661,7 +661,7 @@
No se permiten mensajes temporales.Sólo tú puedes enviar mensajes de voz.Sólo tu contacto puede enviar mensajes de voz.
- EJECUTAR SIMPLEX
+ Ejecutar SimpleXReinicia la aplicación para poder usar la base de datos importada.Introduce la contraseña actual correcta.recepción no permitida
@@ -703,8 +703,8 @@
Aislamiento de transportetachadoAbrir SimpleX
- PROXY SOCKS
- TEMAS
+ Proxy SOCKS
+ TemasPararEsta acción es irreversible. Tu perfil, contactos, mensajes y archivos se perderán.Omitir invitación a miembros
@@ -786,7 +786,7 @@
El perfil sólo se comparte con tus contactos.inicializando…Mensajes omitidos
- CONFIGURACIÓN
+ Configuración¿Parar SimpleX?%s segundo(s)Pulsa para unirte
@@ -794,7 +794,7 @@
Timeout de la conexión TCPTemaEstablece preferencias de grupo
- SOPORTE SIMPLEX CHAT
+ Soporte SimpleX ChatEscribe la contraseña para exportarActualizarActualizar contraseña base de datos
@@ -899,7 +899,7 @@
LlamadasServidores ICEPrivacidad
- MIS DATOS
+ Mis datosBase de datosPuedes iniciar el chat en Configuración / Base de datos o reiniciando la aplicación.Has enviado una invitación de grupo
@@ -990,7 +990,7 @@
Versión de base de datos incompatibleConfirmar actualizaciones de la bases de datosla versión de la base de datos es más reciente que la aplicación, pero no hay migración hacia versión anterior para: %s
- EXPERIMENTAL
+ ExperimentalIDs de la base de datos y opciones de aislamiento de transporte.El archivo se recibirá cuando el contacto termine de subirlo.La imagen se recibirá cuando el contacto termine de subirla.
@@ -1142,7 +1142,7 @@
Mensaje enviadoDejar de compartir¿Dejar de compartir la dirección\?
- COLORES DE LA INTERFAZ
+ Colores de la interfazPuedes crearla más tarde¿Compartir la dirección con los contactos SimpleX?Compartir con contactos SimpleX
@@ -1229,7 +1229,7 @@
sin textoHan ocurrido algunos errores no críticos durante la importación:¿Salir de SimpleX?
- APLICACIÓN
+ AplicaciónReiniciarSalirLas notificaciones dejarán de funcionar hasta que vuelvas a iniciar la aplicación
@@ -1291,7 +1291,7 @@
Activar para todosActivar (conservar anulaciones)Desactivar para todos
- ENVIAR CONFIRMACIONES DE ENTREGA A
+ Enviar confirmaciones de entrega a¡Las confirmaciones de entrega están desactivadas!No activar¡Error al activar confirmaciones de entrega!
@@ -1776,7 +1776,7 @@
Usar siempre enrutamiento privado.Aviso de entrega de mensajeNunca
- ENRUTAMIENTO PRIVADO DE MENSAJES
+ Enrutamiento privado de mensajesLa dirección del servidor es incompatible con la configuración de la red.Con IP desprotegidaClave incorrecta o conexión desconocida - probablemente esta conexión fue eliminada
@@ -1787,7 +1787,7 @@
\n%1$s.Proteger dirección IPSin Tor o VPN, tu dirección IP será visible para los servidores de archivos.
- ARCHIVOS
+ ArchivosLa aplicación pedirá que confirmes las descargas desde servidores de archivos desconocidos (excepto si son .onion o cuando esté habilitado el proxy SOCKS).Colores del chatTema del chat
@@ -2053,7 +2053,7 @@
Por favor, comprueba que el enlace SimpleX es correcto.%1$d archivo(s) se está(n) descargando todavía.%1$d otro(s) error(es) de archivo.
- BASE DE DATOS
+ Base de datosError en reenvío de mensajes¿Reenviar %1$s mensaje(s)?Reenviar mensajes…
@@ -2508,7 +2508,7 @@
Compartir enlace antiguoEl enlace será corto y el perfil del grupo se compartirá mediante el enlace.Actualizar enlace de grupo
- SOLICITUDES DE CONTACTO EN GRUPOS
+ Solicitudes de contacto en gruposconexión solicitada desde el grupo %1$sEsta configuración se aplica al perfil actualMiembro eliminado, no puede aceptar solicitudes
@@ -2597,7 +2597,7 @@
perfil del canal actualizadoEl canal será eliminado para todos los suscriptores. ¡No puede deshacerse!El canal será eliminado para tí. ¡No puede deshacerse!
- CONEXIÓN FALLIDA
+ Conexión fallidaCrear canal públicoCrear canal públicoCrear canal público (BETA)
@@ -2631,12 +2631,12 @@
Hay servidores no conectadosAbrir canalAbrir canal nuevo
- PROPIETARIO
+ PropietarioPropietariosDirecciones predefinidasNombres predefinidosservidor
- SERVIDOR
+ ServidorDirección servidorDirección del servidorEnlace servidor
@@ -2647,7 +2647,7 @@
El servidor requiere autorización para conectar con el servidor, comprueba la contraseña.Alerta del servidorCompartir dirección del servidor
- SUSCRIPTOR
+ SuscriptorSuscriptoresLos suscriptores usan el enlace del servidor para conectarse a los canales.\nLa dirección del servidor se usó para establecer el servidor para el canal.El suscriptor será eliminado del canal. ¡No puede deshacerse!
@@ -2708,7 +2708,7 @@
%1$d servidores han fallado%1$d servidores inactivos%1$d servidores eliminados
- Añadir servidores estará disponible en una versión posterior.
+ Añadir servidores pare retomar el envío.Enlace para un solo contactoPermitir que los miembros chateen con administradores.Permitir que los suscriptores chateen con administradores.
@@ -2798,4 +2798,33 @@
Menú inferiorLas previsualizaciones de enlaces se solicitan a través del proxy SOCKS. Las peticiónes DNS aún pueden usar el DNS local del sistema.Menú superior
+ Tu nuevo canal %1$s está conectado a %2$d de %3$d servidores.\nSi cancelas, el canal será eliminado. Puedes crearlo de nuevo.
+ Añadir
+ Añadir servidor
+ Añadir servidores
+ Cancelar y eliminar el canal
+ Cerrar aplicación
+ %d servidor(es) seleccionados
+ Error al añaidr servidores
+ Error al eliminar mensaje
+ Del historial
+ Si eliges Cerrar, los mensajes no serán recibidos.\nPuedes cambiarlo más tarde desde el menú Apariencia.
+ Mantener Simplex en segundo plano para recibir mensajes.
+ Minimizar
+ Minimizar?
+ Minimizar al cerrar la ventana
+ Sin servidores disponibles
+ Sin servidores
+ Sin servidores seleccionados
+ Salir de SimpleX
+ Servidores añadidos %1$s.
+ El servidor será eliminado del canal. ¡No puede deshacerse!
+ eliminado
+ Eliminar servidor
+ ¿Eliminar el servidor?
+ Seleccionar servidores
+ Ver SimpleX
+ SimpleX
+ SimpleX — %d no leído
+ Este es el último servidor activo. Si lo eliminas los mensajes no llegarán a los suscriptores.
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml
index 3f7d4ff025..a3da005ac6 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml
@@ -781,7 +781,7 @@
خاموشارسال رسید برای %d گروه فعال استارسال رسید برای %d گروه غیرفعال است
- حمایت از SIMPLEX CHAT
+ حمایت از SimpleX Chatپروکسی SOCKSاستفاده از کامپیوترآرشیو پایگاه داده جدید
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml
index 24634192ec..4753cdb9cf 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml
@@ -39,9 +39,9 @@
Salli lähetettyjen viestien peruuttamaton poistaminen.Salli katoavien viestien lähettäminen.Kaikki tiedot poistetaan, kun se syötetään.
- SOVELLUKSEN KUVAKE
+ Sovelluksen kuvakeSovelluksen tietojen varmuuskopiointi
- PUHELUT
+ PuhelutPyydettiin videon vastaanottamistaTunnistauduTunnistautuminen ei ole käytettävissä
@@ -54,7 +54,7 @@
Tietokannan salauksen tunnuslause päivitetään ja tallennetaan Keystoreen.Poista keskusteluprofiili käyttäjällepoistettu
- LAITE
+ Laite%dhYhteysvirheTiedostoa ei voi vastaanottaa
@@ -153,7 +153,7 @@
Vaihda itsetuhotilaaVaihda itsetuhoutuva pääsykoodiSovelluksen salasana korvataan itsetuhoutuvalla pääsykoodilla.
- KESKUSTELUJEN TIETOKANTA
+ Keskustelujen tietokantaKehittäjän työkalutEi pääsyä Keystoreen tietokannan salasanan tallentamiseksiTietokannan tunnus: %d
@@ -308,7 +308,7 @@
Hyväksy kuvat automaattisestiVirheellinen viestin tunnisteVaihda lukitustilaa
- KESKUSTELUT
+ KeskustelutKaikki ryhmän jäsenet pysyvät yhteydessä.Kontaktia ei voi kutsua!valmis
@@ -453,7 +453,7 @@
Virhe käynnistettäessä keskusteluaVirhe keskustelun lopettamisessaVirhe asetuksen muuttamisessa
- KOKEELLINEN
+ KokeellinenPiilota:Kuinka se toimiie2e-salattu videopuhelu
@@ -526,7 +526,7 @@
Kuva tallennettu galleriaanKuva vastaanotetaan, kun kontaktisi on ladannut sen.Tiedosto
- APUA
+ ApuaVirhe tietokannan salauksessaAlenna ja avaa chatEi-aktiivinen ryhmä
@@ -576,7 +576,7 @@
Immuuni roskapostille ja väärinkäytöksilleVirhe vietäessä keskustelujen tietokantaaPiilota
- KONSOLIIN
+ Konsoliinpoistettu ryhmäryhmäprofiili päivitettyVanhentunut kutsu!
@@ -655,7 +655,7 @@
PING-väliProfiili- ja palvelinyhteydetAseta ryhmän asetukset
- PALVELIMET
+ PalvelimetTallenna ja ilmoita kontaktilleTallenna ja ilmoita kontakteilleOhitetut viestit
@@ -715,7 +715,7 @@
ItsetuhoItsetuhoutuva pääsykoodi vaihdettu!Itsetuhoutuva pääsykoodi käytössä!
- SUKAT VÄLITYSPALVELIN
+ SOCKS välityspalvelinUusi tietokanta-arkistoEi vastaanotettuja tai lähetettyjä tiedostojaPoistetaanko tunnuslause Keystoresta\?
@@ -752,7 +752,7 @@
Tallenna ja ilmoita ryhmän jäsenilleLopetaPysäytä keskustelut viedäksesi, tuodaksesi tai poistaaksesi keskustelujen tietokannan. Et voi vastaanottaa ja lähettää viestejä, kun keskustelut on pysäytetty.
- SUORITA CHAT
+ Suorita chatAseta tunnuslause vientiä vartenAnna oikea nykyinen tunnuslause.Palauta
@@ -836,7 +836,7 @@
Avaa SimpleX Chat hyväksyäksesi puhelunei e2e-salaustaTUE SIMPLEX CHATia
- VIESTIT JA TIEDOSTOT
+ Viestit ja tiedostotJaa osoiteVain paikalliset profiilitiedotVastaanotettu viesti
@@ -867,7 +867,7 @@
OKei tietojaKertakutsulinkki
- ASETUKSET
+ AsetuksetUusi tunnuslause…Palauta tietokannan varmuuskopioTietokannan tunnuslauseen muuttamista ei suoritettu loppuun.
@@ -967,7 +967,7 @@
Päivitetty: %sLähetetty kloLähetetty: %s
- JÄSEN
+ JäsenModeroitu klo: %s%s (nykyinen)Vaihda
@@ -1061,7 +1061,7 @@
Hallitset keskustelujasi!Nykyinen profiilisiProfiilisi tallennetaan laitteeseesi ja jaetaan vain kontaktiesi kanssa. SimpleX -palvelimet eivät näe profiiliasi.
- TEEMAT
+ TeematTämä asetus koskee nykyisen keskusteluprofiilisi viestejäTätä toimintoa ei voi kumota - kaikki vastaanotetut ja lähetetyt tiedostot ja media poistetaan. Matalan resoluution kuvat säilyvät.Tuntematon virhe
@@ -1116,7 +1116,7 @@
SMP-palvelimesiXFTP-palvelimesiKäytä SimpleX Chat palvelimia\?
- KÄYTTÖLIITTYMÄN VÄRIT
+ Käyttöliittymän väritPäivitä kuljetuksen eristystila\?Voit luoda sen myöhemminVoit paljastaa piilotetun profiilisi kirjoittamalla koko salasanan Keskusteluprofiilit-sivun hakukenttään.
@@ -1151,7 +1151,7 @@
Video lähetettyOdottaa videotaTämä merkkijono ei ole yhteyslinkki!
- SINÄ
+ SinäPäivitä ja avaa keskusteluAikavyöhykkeen suojaamiseksi kuva-/äänitiedostot käyttävät UTC:tä.Videot ja tiedostot 1 Gt asti
@@ -1228,7 +1228,7 @@
viikkoaIlmoitukset lakkaavat toimimasta, kunnes käynnistät sovelluksen uudelleenSulje
- SOVELLUS
+ SovellusKäynnistä uudelleenSulje\?Pois
@@ -1305,7 +1305,7 @@
Salli kuittaukset\?Kuittauksien lähettäminen on pois käytöstä %d kontakteiltaKuittauksien lähettäminen on käytössä %d kontakteille
- LÄHETÄ TOIMITUSKUITTAUKSET VASTAANOTTAJALLE
+ Lähetä toimituskuittaukset vastaanottajalleturvakoodi on muuttunuthyväksyy salausta…salauksen uudelleenneuvottelu sallittu %s:lle
@@ -1465,7 +1465,7 @@
KameraAvaa asetuksetSuojaa IP-osoite
- TIEDOSTOT
+ TiedostotProfiilikuvattuntematonPoista jäsen
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml
index d95f8ad500..571823140a 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml
@@ -453,7 +453,7 @@
Consomme davantage de batterie L\'app fonctionne toujours en arrière-plan - les notifications s\'affichent instantanément.]]>%1$d message(s) manqué(s)ID du message incorrect
- PARAMÈTRES
+ ParamètresCela peut arriver quand :
\n1. Les messages ont expiré dans le client expéditeur après 2 jours ou sur le serveur après 30 jours.
\n2. Le déchiffrement du message a échoué, car vous ou votre contact avez utilisé une ancienne sauvegarde de base de données.
@@ -487,12 +487,12 @@
Appel en coursAppel terminéVotre vie privée
- APPAREIL
- DISCUSSIONS
+ Appareil
+ DiscussionsOutils du développeur
- ICONE DE L\'APP
+ Icone de l\'appVotre base de données de chat
- LANCER LE CHAT
+ Lancer le chatArrêter le chat \?Redémarrez l\'application pour utiliser la base de données de chat importée.1 jour
@@ -522,10 +522,10 @@
Appels audio et vidéochiffré de bout en boutFonctionnalités expérimentales
- SOCKS PROXY
- THEMES
- MESSAGES ET FICHIERS
- APPELS
+ SOCKS proxy
+ Themes
+ Messages et fichiers
+ AppelsImporter la base de donnéesNouvelle archive de base de donnéesArchives de l\'ancienne base de données
@@ -601,13 +601,13 @@
Protéger l\'écran de l\'appAcceptation automatique des imagesSauvegarde des données de l\'app
- VOUS
- AIDE
- SOUTENEZ SIMPLEX CHAT
+ Vous
+ Aide
+ Soutenez SimpleX ChatMode IncognitoLe chat est en cours d\'exécutionLe chat est arrêté
- BASE DE DONNÉES DU CHAT
+ Base de données du chatPhrase secrète de la base de donnéesExporter la base de donnéesArrêter
@@ -694,7 +694,7 @@
Créer un lienModifier le profil du groupeSupprimer
- MEMBRE
+ MembreMessage dynamique !Envoyer un message dynamiqueEnvoyez un message dynamique - il sera mis à jour pour le⸱s destinataire⸱s au fur et à mesure que vous le tapez
@@ -708,7 +708,7 @@
Erreur lors de la suppression du lien du groupeErreur lors de la création du lien du groupeSeuls les propriétaires du groupe peuvent modifier les préférences du groupe.
- POUR TERMINAL
+ Pour terminalChanger le rôle du groupe \?Son rôle est désormais %s. Tous les membres du groupe en seront informés.Contact vérifié⸱e
@@ -747,7 +747,7 @@
Messages directsSupprimer pour tousVous êtes le seul à pouvoir supprimer des messages de manière irréversible (votre contact peut les marquer comme supprimé). (24 heures)
- SERVEURS
+ ServeursRéception viaSystèmeAutoriser l\'envoi de messages directs aux membres.
@@ -996,7 +996,7 @@
Afficher les options pour les développeursLe fichier sera reçu lorsque votre contact aura terminé de le mettre en ligne.IDs de base de données et option d\'isolement du transport.
- EXPÉRIMENTALE
+ ExpérimentaleCacher :Dévoiler le profil de chatDévoiler le profil
@@ -1102,7 +1102,7 @@
Vous ne perdrez pas vos contacts si vous supprimez votre adresse ultérieurement.Adresse SimpleXVous pouvez accepter ou refuser les demandes de contacts.
- COULEURS DE L\'INTERFACE
+ Couleurs de l\'interfaceVos contacts resteront connectés.Partager l\'adresse avec vos contacts ?Partager avec vos contacts
@@ -1232,7 +1232,7 @@
Arrêt \?Mise à l\'arrêtRedémarrer
- APP
+ AppAbandonnerErreur lors de l\'annulation du changement d\'adresseAbandonner le changement d\'adresse \?
@@ -1251,7 +1251,7 @@
Les membres peuvent envoyer des fichiers et des médias.Les fichiers et les médias sont interdits.Correction non prise en charge par un membre du groupe
- ENVOYER DES ACCUSÉS DE RÉCEPTION AUX
+ Envoyer des accusés de réception auxLe chiffrement fonctionne et le nouvel accord de chiffrement n\'est pas nécessaire. Cela peut provoquer des erreurs de connexion !Encore quelques pointsJustificatifs de réception!
@@ -1776,8 +1776,8 @@
Rabattement du routage des messagesAfficher le statut du messageProtection de l\'adresse IP
- FICHIERS
- ROUTAGE PRIVÉ DES MESSAGES
+ Fichiers
+ Routage privé des messagesErreur au niveau du serveur de destination : %1$sErreur : %1$sCapacité dépassée - le destinataire n\'a pas pu recevoir les messages envoyés précédemment.
@@ -2091,7 +2091,7 @@
Utiliser des identifiants aléatoiresNom d\'utilisateurLes messages seront supprimés - il n\'est pas possible de revenir en arrière !
- BASE DE DONNÉES DU CHAT
+ Base de données du chatMode systèmeServeurDe nouveaux identifiants SOCKS seront utilisées pour chaque serveur.
@@ -2374,7 +2374,7 @@
Se connecterSe connecterconnecté
- CONNEXION ÉCHOUÉE
+ Connexion échouéecontact supprimécontact désactivéle contact devrait accepter…
@@ -2410,7 +2410,7 @@
rejetéRejeter le membre?relais
- RELAIS
+ RelaisAdresse de relaisAdresse de relaisÉchec de la connexion au relais
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hr/strings.xml
index 84e806dda0..55d3e14907 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/hr/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/hr/strings.xml
@@ -29,7 +29,7 @@
prihvati pozivDodeliti dozvoluSlušalice
- POMOĆ
+ PomoćGrupa će biti obrisana za Vas – ovo ne može da se poništi!AkcenatGrupni linkovi
@@ -120,15 +120,15 @@
GreškaNapravi jednokratnu poveznicuNalepiti
- PODEŠAVANJE
+ PodešavanjeProfilne slikeRazumeoOdstranjenoodstranjenoNapraviti
- PORUKE I DATOTEKE
+ Poruke i datotekePoruka
- SERVERI
+ ServeriOdstraniti profil razgovoraadministratoriNasumično
@@ -240,7 +240,7 @@
PreuzimanjeNapredna podešavanjaPoziv u toku
- POZIVI
+ PoziviBlokiraj članove grupeNepoznati serveri!Datoteka
@@ -253,7 +253,7 @@
%d poruka blokiranopovezivanjePovezan telefon
- VI
+ ViZamućeno za bolju privatnost.%d meseca(i)Poziv završen
@@ -295,7 +295,7 @@
%d minut(a)Proveri ažuriranjeStabilno
- DATOTEKE
+ Datoteke%s otpremljenoOnemogućiti obavještenja%s nije verifikovan
@@ -312,7 +312,7 @@
Skenirati QR kodServerOnemogućiti
- BAZA PODATAKA CHATA
+ Baza podataka chataonemogućenoGreška pri uvoženju temeDatoteke i medijski sadržaji su zabranjeni.
@@ -328,7 +328,7 @@
Ili skenirati QR kodOnemogućenoAplikacija
- RAZGOVORI
+ RazgovoriDatoteke i medijski sadržaji su zabranjeni!Poruke koje nestaju su zabranjene u ovom razgovoru.Chat je zaustavljen
@@ -370,7 +370,7 @@
QR kodChat je pokrenutUvesti bazu podataka
- BAZA PODATAKA CHATA
+ Baza podataka chataChat je zaustavljen%s, %s i %d ostali članovi povezaniUvoz neuspešan
@@ -428,7 +428,7 @@
SimpleX adresaSimpleX LogoPrikazati:
- UREĐAJ
+ UređajNova porukaSekundarniKontakti
@@ -474,7 +474,7 @@
OmiljenNikadaVeza
- TEME
+ TemeAudio/video pozivinešifrovanje ok
@@ -491,7 +491,7 @@
SimpleX AdresaSačuvatisimplexmq: v%s (%2s)
- EKSPERIMENTALNO
+ EksperimentalnonikadaOčistitiZahvaljujući korisnicima – doprinesi pomoću Weblate!
@@ -694,7 +694,7 @@
Izabrati profil razgovoraSkenirati QR kod serveraNapredna mrežna podešavanja
- SOCKS PROXY
+ SOCKS proxyAnonimni režimbroj PINGObnoviti statistiku?
@@ -725,7 +725,7 @@
Arhiviraj bazu podatakaPeriodičnoUkloniti
- ČLAN
+ ČlanPristupanje grupiSMP serverPozvati u razgovor
@@ -986,7 +986,7 @@
Novi serverPrikazati procenteNapustiti bez čuvanja
- POKRENUTI RAZGOVOR
+ Pokrenuti razgovorOdblokirati člana za sve?Priprema za preuzimanjeProxied(posredovan)
@@ -1260,9 +1260,9 @@
Pin kod postavljen!Svi podaci u aplikaciji su odstranjeni.Pin kod aplikacije je zamenjen pin kodom za samouništenje.
- POTPORI SIMPLEX CHAT
+ Potpori SimpleX ChatOblik poruke
- IKONA APLIKACIJE
+ Ikona aplikacijePristupna fraza baze podatakaOdrediti pristupnu frazuBaza podataka će biti šifrovana.
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml
index 2df64ae590..b90d79fb3b 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml
@@ -38,14 +38,14 @@
%s visszavonvaElőre beállított kiszolgálók hozzáadásaA hívások kezdeményezése le van tiltva.
- Az összes partneréhez és csoporttaghoz külön TCP-kapcsolat (és SOCKS-hitelesítési adat) lesz használva.\nMegjegyzés: ha sok kapcsolata van, akkor az akkumulátor-használat és az adatforgalom jelentősen megnövekedhet, és néhány kapcsolódási kísérlet sikertelen lehet.]]>
+ Az összes partneréhez és csoporttaghoz külön TCP-kapcsolat (és SOCKS-hitelesítési adat) lesz használva.\nMegjegyzés: ha sok kapcsolata van, akkor az akkumulátor-használat és az adatforgalom jelentősen megnövekedhet, és néhány kapcsolódási kísérlet sikertelen lehet.]]>hivatkozáselőnézet visszavonásaAz összes csevegési profiljához az alkalmazásban külön TCP-kapcsolat (és SOCKS-hitelesítési adat) lesz használva.]]>Mindkét fél küldhet eltűnő üzeneteket.Az Android Keystore-t a jelmondat biztonságos tárolására használják – lehetővé teszi az értesítési szolgáltatás működését.Hibás az üzenet kivonataHáttér
- Megjegyzés: az üzenet- és a fájlátjátszók SOCKS proxyn keresztül kapcsolódnak. A hívások pedig közvetlen kapcsolatot használnak.]]>
+ Megjegyzés: az üzenet- és a fájlátjátszók SOCKS proxyn keresztül kapcsolódnak. A hívások pedig közvetlen kapcsolatot használnak.]]>Alkalmazásadatok biztonsági mentéseAz adatbázis előkészítése sikertelenAz összes partnerével továbbra is kapcsolatban marad. A profilfrissítés el lesz küldve a partnerei számára.
@@ -71,7 +71,7 @@
Továbbfejlesztett csoportokAz összes üzenet törölve lesz – ez a művelet nem vonható vissza! Az üzenetek CSAK az Ön számára törlődnek.A hívás véget ért
- HÍVÁSOK
+ Hívásokés további %d eseményCímA csatlakozás folyamatban van a csoporthoz!
@@ -91,7 +91,7 @@
Nem lehet meghívni a partnert!hibás az üzenet azonosítójaPartneri kapcsolatkérések automatikus elfogadása
- Megjegyzés: NEM fogja tudni helyreállítani vagy módosítani a jelmondatot abban az esetben, ha elveszíti.]]>
+ Megjegyzés: NEM fogja tudni helyreállítani vagy módosítani a jelmondatot abban az esetben, ha elveszíti.]]>hívás…További másodlagos színHozzáadás egy másik eszközhöz
@@ -114,7 +114,7 @@
Az alkalmazásjelkód helyettesítve lesz egy önmegsemmisítő jelkóddal.Arab, bolgár, finn, héber, thai és ukrán – köszönet a felhasználóknak és a Weblate-nek.Engedélyezi a hangüzeneteket?
- Mindig legyen használva átjátszó
+ Átjátszó használata minden esetbenmindigA hívás már véget ért!Engedélyezés
@@ -149,13 +149,13 @@
hívás folyamatbanKépek automatikus elfogadásaA hívások kezdeményezése engedélyezve van a partnerei számára.
- ALKALMAZÁSIKON
+ AlkalmazásikonKiszolgáló hozzáadása QR-kód beolvasásával.Az eltűnő üzenetek küldése engedélyezve van.Az eltűnő üzenetek küldése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi.Hang kikapcsolvaA közvetlen üzenetek küldése a tagok között engedélyezve van.
- ALKALMAZÁS
+ AlkalmazásHívás folyamatbanMindkét fél hozzáadhat az üzenetekhez reakciókat.Mindkét fél tud hívásokat kezdeményezni.
@@ -168,7 +168,7 @@
hanghívás (végpontok között NEM titkosított)letiltvaMódosítja az adatbázis jelmondatát?
- kapcsolódva
+ kapcsolódottJelkód módosításaa következőre módosította %s szerepkörét: „%s”Fogadási cím módosítása
@@ -182,9 +182,9 @@
KapcsolódáskapcsolódottTársított hordozható eszköz
- kapcsolódva
+ kapcsolódottSzerepkör módosítása
- Kapcsolódva
+ KapcsolódottHitelesítési adat megerősítéseMódosítja a fogadási címet?módosította a címet az Ön számára
@@ -300,7 +300,7 @@
Hívás kapcsolásaTörli a fájlokat és a médiatartalmakat?kész
- CSEVEGÉSI ADATBÁZIS
+ Csevegési adatbázisÖnmegsemmisítő jelkód módosításaVárólista létrehozásaszínezett
@@ -313,7 +313,7 @@
kapcsolódásEgyéni időközKapcsolódás inkognitóban
- CSEVEGÉSEK
+ CsevegésekÚj profil létrehozása a számítógépes alkalmazásban. 💻kapcsolódás (bejelentve)kapcsolódás…
@@ -393,7 +393,7 @@
Ne jelenjen meg újraSimpleX-zár kikapcsolásavégpontok között titkosított
- ESZKÖZ
+ Eszközvégpontok között titkosított videóhívásközvetlenSzámítógép
@@ -522,7 +522,7 @@
Akkor is, ha le van tiltva a beszélgetésben.Gyorsabb csatlakozás és megbízhatóbb üzenetkézbesítés.Zárolás engedélyezése
- SÚGÓ
+ SúgóTeljesen decentralizált – csak a tagok számára látható.Fájl: %sHívás befejezése
@@ -530,7 +530,7 @@
Fájl mentveKapcsolat javítása?Fájlok és médiatartalmak
- KONZOLHOZ
+ KonzolhozNem sikerült a titkosítást újraegyeztetni.Hiba történt a felhasználói profil törlésekorCsoporttag általi javítás nem támogatott
@@ -579,7 +579,7 @@
A csoport teljes neve:súgóÖnmegsemmisítő jelkód engedélyezése
- KÍSÉRLETI
+ KísérletiHiba történt a cím módosításának megszakításakorHiba történt a fájl fogadásakortitkosítása rendben van
@@ -722,7 +722,7 @@
A reakciók hozzáadása az üzenetekhez le van tiltva.Nemnincs szöveg
- TAG
+ TagHogyan befolyásolja az akkumulátortÚj tag szerepköreKikapcsolva
@@ -842,7 +842,7 @@
Név és üzenetAz értesítések csak az alkalmazás bezárásáig érkeznek!Információ
- ÜZENETEK ÉS FÁJLOK
+ Üzenetek és fájloktagPrivát kapcsolat létrehozása%s moderálta ezt az üzenetet
@@ -918,7 +918,7 @@
Üdvözlőüzenet%s, %s és további %d tag kapcsolódottCsak a partnere kezdeményezhet hívásokat.
- TÉMÁK
+ TémákTúl sok videó!Üdvözöljük!Önmegsemmisítő jelkód
@@ -963,7 +963,7 @@
Ön elfogadta a kapcsolatotElutasításPartner nevének és az üzenet tartalmának megjelenítése
- BEÁLLÍTÁSOK
+ BeállításokProfiljelszó mentéseMegállítja a fájlküldést?Leválasztja a számítógépet?
@@ -1007,7 +1007,7 @@
QR-kód beolvasásaKiszolgáló teszteléseKüldjön nekünk e-mailt
- KISZOLGÁLÓK
+ KiszolgálókKiszolgálók teszteléseJelkód beviteleRendszer
@@ -1018,12 +1018,12 @@
A reakciók hozzáadása az üzenethez le van tiltva.Véletlenszerű jelmondat használataegyenrangú
- CSEVEGÉSI SZOLGÁLTATÁS INDÍTÁSA
+ Csevegési szolgáltatás indításaKapott hivatkozás beillesztéseMenti a kiszolgálókat?A SimpleX Chat biztonsága a Trail of Bits által lett auditálva.
- frissítette a csoportprofilt
- SIMPLEX CHAT TÁMOGATÁSA
+ frissítette a csoport profilját
+ SimpleX Chat támogatásaSimpleX Chat szolgáltatásÖn megfigyelő%s ellenőrizve
@@ -1072,10 +1072,10 @@
Egyszer használható SimpleX meghívóHívásoknem sikerült elküldeni
- KEZELŐFELÜLET SZÍNEI
+ Kezelőfelület színeiAdja meg a korábbi jelszót az adatbázis biztonsági mentésének visszaállítása után. Ez a művelet nem vonható vissza.Másodlagos szín
- SOCKS PROXY
+ SOCKS proxyMentésÚjraindításSMP-kiszolgálók
@@ -1106,7 +1106,7 @@
igenHangüzenetTársítás számítógéppel
- PROFIL
+ Profil%d-s portKapcsolódás egy hivatkozáson keresztülCím megosztása
@@ -1295,7 +1295,7 @@
Nem sikerült ellenőrizni; próbálja meg újra.Az üzenet az összes tag számára moderáltként lesz megjelölve.Értesítések fogadásához adja meg az adatbázis jelmondatát
- A teszt a(z) %s lépésnél sikertelen volt.
+ A teszt a(z) %s. lépésnél sikertelen volt.Az alkalmazás elindításához vagy 30 másodpercnyi háttérben töltött idő után, az alkalmazáshoz való visszatéréshez hitelesítésre lesz szükség.Az üzenet az összes tag számára törölve lesz.A videó nem dekódolható. Próbálja ki egy másik videóval, vagy lépjen kapcsolatba a fejlesztőkkel.
@@ -1457,7 +1457,7 @@
Kis csoportok (legfeljebb 20 tag)Az Ön által elfogadott kapcsolat vissza lesz vonva!Élő üzenet küldése – az üzenet a címzett(ek) számára valós időben frissül, ahogy Ön beírja az üzenetet
- A KÉZBESÍTÉSI JELENTÉSEKET A KÖVETKEZŐ CÍMRE KELL KÜLDENI
+ A kézbesítési jelentéseket a következő címre kell küldeniA következő üzenet azonosítója érvénytelen (kisebb vagy egyenlő az előzővel).\nEz valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő.Az eszköz neve meg lesz osztva a társított hordozható eszközön használt alkalmazással.A címzettek a beírás közben látják a szövegváltozásokat.
@@ -1571,8 +1571,8 @@
feloldotta %s letiltásátÖn feloldotta %s letiltásátletiltva
- letiltva az adminisztrátor által
- Letiltva az adminisztrátor által
+ az adminisztrátor letiltotta
+ Az adminisztrátor letiltottaletiltotta őt: %sLetiltásAz összes tag számára letiltja a tagot?
@@ -1649,14 +1649,14 @@
Átköltöztetés ideEszköz átköltöztetéseÁtköltöztetés egy másik eszközre
- Figyelmeztetés: az archívum törölve lesz.]]>
+ Figyelmeztetés: az archívum törölve lesz.]]>Átköltöztetés egy másik eszközrőlKvantumbiztos titkosításMegpróbálhatja még egyszer.Átköltöztetés készÁtköltöztetés egy másik eszközre QR-kód használatával.Átköltöztetés
- Megjegyzés: ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja a partnereitől érkező üzenetek visszafejtését.]]>
+ Megjegyzés: ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja a partnereitől érkező üzenetek visszafejtését.]]>Megpróbálhatja még egyszer.Érvénytelen hivatkozásvégpontok közötti kvantumbiztos titkosítás
@@ -1720,11 +1720,11 @@
Profilkép alakzataNégyzet, kör vagy bármi a kettő között.Célkiszolgáló-hiba: %1$s
- Továbbítókiszolgáló: %1$s\nHiba: %2$s
+ Továbbító kiszolgáló: %1$s\nHiba: %2$sHálózati problémák – az üzenet többszöri elküldési kísérlet után lejárt.A kiszolgáló verziója nem kompatibilis a hálózati beállításokkal.Érvénytelen kulcs vagy ismeretlen kapcsolat – valószínűleg ez a kapcsolat törlődött.
- Továbbítókiszolgáló: %1$s\nCélkiszolgáló-hiba: %2$s
+ Továbbító kiszolgáló: %1$s\nCélkiszolgáló-hiba: %2$sHiba: %1$sKapacitás túllépés – a címzett nem kapta meg a korábban elküldött üzeneteket.Üzenetkézbesítési figyelmeztetés
@@ -1738,20 +1738,20 @@
NemNem védettIgen
- NE legyen használva privát útválasztás.
+ Privát útválasztás használatának elkerülése.Privát útválasztásPrivát útválasztás használata az ismeretlen kiszolgálókhoz.
- Mindig legyen használva privát útválasztás.
+ Privát útválasztás használata minden esetben.Üzenet-útválasztási módKözvetlen üzenetküldés, ha az IP-cím védett és a saját kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást.Közvetlen üzenetküldés, ha a saját kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást.Az IP-cím védelmének érdekében a privát útválasztás az SMP-kiszolgálókat használja az üzenetek kézbesítéséhez.Üzenet-útválasztási tartalék
- PRIVÁT ÜZENET-ÚTVÁLASZTÁS
+ Privát üzenet-útválasztásPrivát útválasztás használata az ismeretlen kiszolgálókkal, ha az IP-cím nem védett.NE küldjön üzeneteket közvetlenül, még akkor sem, ha a saját kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást.Tor vagy VPN nélkül az IP-címe láthatóvá válik a fájlkiszolgálók számára.
- FÁJLOK
+ FájlokIP-cím védelmeAz alkalmazás kérni fogja az ismeretlen fájlkiszolgálókról történő letöltések megerősítését (kivéve, ha az .onion vagy a SOCKS proxy engedélyezve van).Ismeretlen kiszolgálók!
@@ -1874,7 +1874,7 @@
Kiszolgáló-beállítások megnyitásaKiszolgáló címeFeltöltési hibák
- Visszaigazolt
+ VisszaigazolvaVisszaigazolási hibákkísérletekTörölt töredékek
@@ -1926,7 +1926,7 @@
Ezen verzió kihagyásaHa értesítést szeretne kapni az új kiadásokról, kapcsolja be a stabil vagy béta verziók időszakos ellenőrzését.Új verzió érhető el: %s
- A frissítés letöltése megszakítva
+ Frissítésletöltés visszavonvaBétaLetiltásLetiltva
@@ -2019,7 +2019,7 @@
Az üzenetek törölve lesznek – ez a művelet nem vonható vissza!Eltávolítja az archívumot?A feltöltött adatbázis-archívum véglegesen el lesz távolítva a kiszolgálókról.
- CSEVEGÉSI ADATBÁZIS
+ Csevegési adatbázisProfil megosztásaRendszerbeállítások használataCsevegési profil kiválasztása
@@ -2194,7 +2194,7 @@
A tagok közötti közvetlen üzenetek le vannak tiltva.Üzleti csevegésekSaját ügyfeleinek adatvédelme.
- %1$s.]]>
+ %1$s.]]>A csevegés már létezik!Csökkentse az üzenet méretét, és küldje el újra.Üzenetek ellenőrzése 10 percenként
@@ -2270,7 +2270,7 @@
Tagok jelentései1 jelentésJelentések
- %s által archivált jelentés
+ %s archiválta a jelentést%d jelentésKéretlen tartalomA tartalom sérti a használati feltételeket
@@ -2350,7 +2350,7 @@
KikapcsolvaElőre beállított kiszolgálókA 443-as TCP-port használata kizárólag az előre beállított kiszolgálókhoz.
- Hiba a tag befogadásakor
+ Hiba történt a tag befogadásakor%d csevegés a tagokkal%d üzenet1 csevegés egy taggal
@@ -2382,7 +2382,7 @@
Ön befogadta ezt a tagotBefogadás tagkéntbefogadta őt: %1$s
- áttekintve a moderátorok által
+ a moderátorok áttekintettéknem lehet üzeneteket küldenipartner letiltvacsoport törölve
@@ -2407,7 +2407,7 @@
Hiba történt a partneri kapcsolatkérés elutasításakorHiba történt a csevegés megnyitásakorHiba történt a csoport megnyitásakor
- Hiba a profil módosításakor
+ Hiba történt a profil módosításakorMegnyitás a csatlakozáshozMegnyitás a kapcsolódáshozMegnyitás az elfogadáshoz
@@ -2476,7 +2476,7 @@
A hivatkozás rövid lesz és a csoportprofil meg lesz osztva a hivatkozáson keresztül.Régi cím megosztásaRégi (hosszú) hivatkozás megosztása
- PARTNERI KAPCSOLATKÉRÉSEK A CSOPORTOKBÓL
+ Partneri kapcsolatkérések a csoportokbólA tag törölve lett – nem lehet elfogadni a kérésta(z) %1$s nevű csoportból partneri kapcsolatot kértEz a beállítás a jelenlegi profiljára vonatkozik
@@ -2496,7 +2496,7 @@
Teljes hivatkozás megnyitásaNyomonkövetési paraméterek eltávolítása a hivatkozásokbólSimpleX-átjátszó címe
- Hiba a csevegés olvasottként való megjelölésekor
+ Hiba történt a csevegés olvasottként való megjelölésekorA célkiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %1$s.A továbbító kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %1$s.A kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %1$s.
@@ -2519,7 +2519,7 @@
Hangüzenetek kereséseVideókHangüzenetek
- NEM SIKERÜLT LÉTREHOZNI A KAPCSOLATOT
+ Nem sikerült létrehozni a kapcsolatotsikertelenHa csatornákat hozott létre vagy csak csatlakozott hozzájuk, akkor azok véglegesen le fognak állni.aktív
@@ -2545,7 +2545,7 @@
meghívaCsatorna megnyitásaÚj csatorna megnyitása
- TULAJDONOS
+ TulajdonosTulajdonosokCsatorna elhagyásaElhagyja a csatornát?
@@ -2555,7 +2555,7 @@
ÖnSaját csatornaSaját csatorna
- FELIRATKOZÓ
+ FeliratkozóFeliratkozók%1$d feliratkozó%1$d feliratkozó
@@ -2597,7 +2597,7 @@
Ellenőrizze az átjátszó nevét, és próbálja újra.Érvénytelen az átjátszó címe!Ellenőrizze az átjátszó címét, és próbálja újra.
- Hiba az átjátszó hozzáadásakor
+ Hiba történt az átjátszó hozzáadásakorCsevegési átjátszókA csevegési átjátszók továbbítják az üzeneteket az Ön által létrehozott csatornákban.Csevegési átjátszók
@@ -2605,9 +2605,9 @@
A csevegési átjátszók továbbítják az üzeneteket a csatorna feliratkozóinak.%1$d/%2$d átjátszó aktív, %3$d sikertelen%1$d/%2$d átjátszó aktív
- %1$d/%2$d átjátszó kapcsolódva, %3$d hiba
- %1$d/%2$d átjátszó kapcsolódva
- ÁTJÁTSZÓ
+ %1$d/%2$d átjátszó kapcsolódott, %3$d hiba
+ %1$d/%2$d átjátszó kapcsolódott
+ ÁtjátszóÁtjátszóhivatkozásÁtjátszó címea következőn keresztül: %1$s
@@ -2616,7 +2616,7 @@
Ön ezen az átjátszóhivatkozáson keresztül kapcsolódott a csatornához.Feliratkozó eltávolításaAz összes feliratkozó számára letiltja a feliratkozót?
- Hiba a csatorna létrehozásakor
+ Hiba történt a csatorna létrehozásakorVisszavonja a csatorna létrehozását?Engedélyezzen legalább egy csevegési átjátszót a csatorna létrehozásához.A(z) %1$s nevű profilja meg lesz osztva a csatorna átjátszóival és feliratkozóival.\nAz átjátszók hozzáférhetnek a csatornaüzenetekhez.
@@ -2627,7 +2627,7 @@
Átjátszó címeEz egy csevegési átjátszó címe, nem használható kapcsolódásra.%1$s nevű csatornához!]]>
- Hiba a csatorna megnyitásakor
+ Hiba történt a csatorna megnyitásakorAz összes feliratkozó számára feloldja a feliratkozó letiltását?Átjátszó tesztelése a nevének lekéréséhez.]]>Csatorna teljes neve:
@@ -2636,11 +2636,11 @@
%d csatornaeseménytörölt csatornahiba: %s
- Hiba a csatornaprofil mentésekor
+ Hiba történt a csatornaprofil mentésekorÜzenethibaMentés és a csatorna feliratkozóinak értesítéseCsatornaprofil mentése
- frissített csatornaprofil
+ frissítette a csatorna profiljátAz alkalmazás %1$d sikertelen letöltési kísérlet után eltávolította ezt az üzenetet.eltávolítva (%1$d kísérlet)A csatorna ideiglenesen nem érhető el
@@ -2658,12 +2658,12 @@
%1$d/%2$d átjátszó aktív, %3$d hiba%1$d/%2$d átjátszó kapcsolódott, %3$d átjátszóhoz nem sikerült kapcsolódni%1$d/%2$d átjátszó kapcsolódott, %3$d eltávolítva
- Az átjátszók hozzáadása később lesz támogatott.
+ Átjátszók hozzáadása az üzenetküldés helyreállításához.Várakozás a csatorna tulajdonosára az átjátszók hozzáadásához.Üzleti címCsatornahivatkozásKapcsolattartási cím
- Hiba a csatorna megosztásakor
+ Hiba történt a csatorna megosztásakor(a tulajdonostól)CsoporthivatkozásHivatkozás aláírása ellenőrizve.
@@ -2692,7 +2692,7 @@
Saját hivatkozás létrehozásaBárki számára, aki el szeretné érni ÖntPartner meghívása privátban
- Hagyja, hogy valaki elérje Önt
+ Legyen elérhető mások számáraVagy mutassa meg a QR-kódot személyesen vagy videóhíváson keresztül.Vagy használja ezt a QR-kódot – nyomtassa ki vagy mutassa meg online.Küldje el a hivatkozást bármilyen üzenetváltó alkalmazáson keresztül – ez egy biztonságos módszer – és kérje meg a partnerét, hogy illessze be a SimpleX alkalmazásba.
@@ -2702,7 +2702,7 @@
Nonprofit irányítás- Hivatkozások előnézetének küldése.\n- SOCKS proxy használata, ha engedélyezve van.\n- Hiperhivatkozásokon keresztüli adathalászat megakadályozása.\n- Hivatkozások nyomonkövetési paramétereinek eltávolítása.Tulajdonjog: saját átjátszókat üzemeltethet.
- Adatvédelem: tulajdonosok és előfizetők számára.
+ Adatvédelem: tulajdonosok és feliratkozók számára.Nyilvános csatornák – mondja el szabadon a véleményét 🚀Megbízhatóság: több átjátszó is használható csatornánként.Biztonságos webhivatkozások
@@ -2711,13 +2711,13 @@
Az új felhasználók számára egyszerűbbé tettük a kapcsolatok létrehozását.Fiók nélkül születtünk.Senki sem követte nyomon a beszélgetéseinket. Senki sem készített térképet arról, hogy merre jártunk. A magánéletünk nem csak egy funkció volt, hanem az életmódunk.
- Aztán felléptünk az internetre, és minden platform kért belőlünk egy darabot - nevet, telefonszámot, baráti kapcsolatokat. Elfogadtuk, hogy a kommunikáció ára az, hogy mások megtudják, hogy kivel beszélünk. Minden generáció, az emberek és a technológia is eddig így működött - telefon, e-mail, üzenetküldő programok, közösségi média. Úgy tűnt, ez az egyetlen lehetséges mód.
+ Aztán felléptünk az internetre, és minden platform kért belőlünk egy darabot– nevet, telefonszámot, baráti kapcsolatokat. Elfogadtuk, hogy a kommunikáció ára az, hogy mások megtudják, hogy kivel beszélünk. Minden generáció, az emberek és a technológia is eddig így működött – telefon, e-mail, üzenetküldő programok, közösségi média. Úgy tűnt, ez az egyetlen lehetséges mód.De van egy másik lehetőség is. Egy hálózat, amelyben nincsenek telefonszámok. Nincsenek felhasználónevek. Nincsenek fiókok. Nincsenek semmiféle felhasználói azonosítók. Egy hálózat, amely összeköti az embereket és titkosított üzeneteket továbbít, anélkül, hogy tudná, ki csatlakozik hozzá.
- Nem egy jobb zár mások ajtaján. Nem egy kedvesebb házmester, aki tiszteletben tartja az Ön magánéletét, de mégis nyilvántartást vezet minden látogatójáról. Ön itt nem csak egy vendég. Ön itt otthon van. Nincs az a hatalom, amely beléphetne ide - Ön itt szuverén.
+ Nem egy jobb zár mások ajtaján. Nem egy kedvesebb házmester, aki tiszteletben tartja az Ön magánéletét, de mégis nyilvántartást vezet minden látogatójáról. Ön itt nem csak egy vendég. Ön itt otthon van. Nincs az a hatalom, amely beléphetne ide – Ön itt szuverén.A beszélgetései Önhöz tartoznak, ahogy az internet megjelenése előtt is mindig így volt. A hálózat nem egy hely, amelyet meglátogat. Ez egy olyan hely, amelyet Ön hoz létre saját magának. És senki sem veheti el Öntől, függetlenül attól, hogy privát vagy nyilvános.
- A legrégebbi emberi szabadság - beszélgetni az emberekkel, anélkül, hogy mások megfigyelnének - olyan infrastruktúrán alapul, amely nem tudja elárulni.
+ A legrégebbi emberi szabadság – beszélgetni az emberekkel, anélkül, hogy mások megfigyelnének – olyan infrastruktúrán alapul, amely nem tudja elárulni.Mert felszámoltuk a lehetőségét is annak, hogy megtudjuk, Ön kicsoda. Így az önrendelkezése soha nem kerülhet idegen kezekbe.
- Legyen szabad a saját hálózatában.
+ Váljon szabaddá a saját hálózatában.Feliratkozók jelentéseiA közvetlen üzenetek küldése a feliratkozók között engedélyezve van.
@@ -2737,7 +2737,7 @@
Az előzmények nem lesznek elküldve az új feliratkozók számára.A csevegés az adminisztrátorokkal engedélyezve van a tagok számára.A csevegés az adminisztrátorokkal engedélyezve van a feliratkozók számára.
- Váljon szabaddá\na saját hálózatában.
+ Váljon szabaddá\na saját hálózatábanA csevegés az adminisztrátorokkal le van tiltva.A nyilvános csatornákban az adminisztrátorokkal való csevegések nem rendelkeznek végpontok közötti titkosítással – csak megbízható csevegési átjátszókkal használja őket.A csevegés a tagokkal le van tiltva
@@ -2758,12 +2758,46 @@
A csevegés az adminisztrátorokkal le van tiltva.Értesítések beállításaÚtválasztók beállítása
- A feliratkozók cseveghetnek az adminisztrátorokkal
+ A feliratkozók cseveghetnek az adminisztrátorokkal.Miért jött létre a SimpleX?Saját hálózat
- Profil létrehozása
+ Saját profil létrehozásaAz első hálózat, ahol Ön birtokolja\na saját kapcsolatait és csoportjait.Alsó sávA hivatkozások előnézetét SOCKS proxyn keresztül kéri le a kliens. A DNS-lekérdezés viszont továbbra is történhet helyi szinten, a saját DNS-kiszolgálón keresztül.Felső sáv
+ Az új %1$s nevű csatornája %3$d átjátszóból %2$d átjátszóhoz kapcsolódik.\nHa visszavonja, akkor a csatorna törlődni fog – de később újra létrehozhatja.
+ Visszavonás és a csatorna törlése
+ Hozzáadás
+ Átjátszó hozzáadása
+ Átjátszók hozzáadása
+ %d átjátszó kiválasztva
+ Hiba történt az átjátszók hozzáadásakor
+ Nincsenek elérhető átjátszók
+ Nincsenek átjátszók
+ Nincsenek átjátszók kiválasztva
+ Átjátszók hozzáadva: %1$s.
+ Az átjátszó el lesz távolítva a csatornából – ez a művelet nem vonható vissza!
+ eltávolítva
+ Átjátszó eltávolítása
+ Eltávolítja az átjátszót?
+ Átjátszók kiválasztása
+ Ez az utolsó aktív átjátszó. Ha eltávolítja, akkor azzal megakadályozza az üzenetek eljuttatását a feliratkozóknak.
+ Alkalmazás bezárása
+ Ha a bezárás mellett dönt, az üzenetek nem fognak megérkezni.\nEzt később a „Megjelenés” beállításaiban módosíthatja.
+ Hagyja a SimpleX-et a háttérben futni az üzenetek fogadásához.
+ Kilépés a SimpleXből
+ SimpleX megjelenítése
+ SimpleX
+ SimpleX – %d olvasatlan üzenet
+ Kicsinyítés az értesítési területre
+ Biztosan kicsinyíteni szeretné az értesítési területre?
+ Kicsinyítés az értesítési területre az ablak bezárásakor
+ Hiba történt az üzenet törlésekor
+ Az előzményekből
+ Állapot
+ elutasítva
+ az átjátszó üzemeltetője elutasította
+ Az alkalmazás már fut
+ Lehet, hogy egy másik alkalmazáspéldány fut, vagy nem zárult be megfelelően. Így is elindítja?
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/badge_investor.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/badge_investor.svg
new file mode 100644
index 0000000000..330da9b50d
--- /dev/null
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/badge_investor.svg
@@ -0,0 +1,12 @@
+
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/badge_legend.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/badge_legend.svg
new file mode 100644
index 0000000000..7f892cd25c
--- /dev/null
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/badge_legend.svg
@@ -0,0 +1,12 @@
+
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/badge_supporter.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/badge_supporter.svg
new file mode 100644
index 0000000000..9ebdc15c11
--- /dev/null
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/badge_supporter.svg
@@ -0,0 +1,12 @@
+
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml
index 60ed7db384..70c31f5399 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml
@@ -44,7 +44,7 @@
Tambahkan ke perangkat lainBolehSelalu
- APLIKASI
+ AplikasiTampilanTentang SimpleX ChatTerima
@@ -293,7 +293,7 @@
Terlalu banyak gambar!caripanggilan
- PENGATURAN
+ PengaturanUntuk semua orangHentikan berkasCabut berkas
@@ -643,9 +643,9 @@
Kode sandi hapus otomatisAktifkan hapus otomatisPasang kode sandi
- BANTUAN
- DUKUNG SIMPLEX CHAT
- PANGGILAN
+ Bantuan
+ Dukung SimpleX Chat
+ PanggilanMulai ulang aplikasi untuk buat profil obrolan baru.Hapus pesankeluar
@@ -729,9 +729,9 @@
SedangBuram mediaKuat
- ANDA
+ AndaLunak
- BASIS DATA OBROLAN
+ Basis data obrolanSetel frasa sandi untuk dieksporBuka folder basis datamenghapus anda
@@ -968,7 +968,7 @@
Build aplikasi: %sVersi inti: v%sKetika IP disembunyikan
- WARNA ANTARMUKA
+ Warna antarmukaFallback perutean pesanMode routing pesanRouting pribadi
@@ -1003,7 +1003,7 @@
Server ICE AndaServer ICE WebRTCJika Anda memasukkan kode sandi hapus otomatis saat membuka aplikasi:
- IKON APLIKASI
+ Ikon aplikasiAplikasi akan meminta untuk mengonfirmasi unduhan dari server berkas yang tidak dikenal (kecuali .onion atau saat proxy SOCKS diaktifkan).Reaksi pesan dilarang dalam obrolan ini.Pindah ke perangkat lain
@@ -1020,8 +1020,8 @@
Panggilan tak terjawabPanggilan ditolakID pesan berikutnya salah (kurang atau sama dengan yang sebelumnya).\nHal ini dapat terjadi karena beberapa bug atau ketika koneksi terganggu.
- TEMA
- KIRIM TANDA TERIMA KIRIMAN KE
+ Tema
+ Kirim tanda terima kiriman keHal ini dapat terjadi ketika Anda atau koneksi Anda menggunakan cadangan basis data lama.Android Keystore digunakan untuk menyimpan frasa sandi dengan aman - memungkinkan layanan notifikasi berfungsi.Hapus
@@ -1134,9 +1134,9 @@
DikenalMenunggu gambarMenunggu video
- PERANGKAT
- OBROLAN
- BERKAS
+ Perangkat
+ Obrolan
+ BerkasReset semua petunjukGagal menambah anggotaGagal gabung ke grup
@@ -1281,7 +1281,7 @@
Buka blokir anggota untuk semua?Buka untuk semuaDiblokir oleh admin
- ANGGOTA
+ AnggotaHapus anggotaStatus pesan: %sStatus berkas: %s
@@ -1326,7 +1326,7 @@
Perbaikan tidak didukung oleh kontakObrolanTerima kondisi
- SERVER
+ ServerBuat grupNama lengkap grup:Simpan profil grup
@@ -1466,7 +1466,7 @@
Terbaik untuk baterai. Anda akan menerima notifikasi saat aplikasi sedang berjalan (TANPA layanan latar belakang).]]>Baik untuk baterai. Aplikasi memeriksa pesan setiap 10 menit. Anda mungkin melewatkan panggilan atau pesan penting.]]>Tema obrolan
- BASIS DATA OBROLAN
+ Basis data obrolanBasis data dienkripsi menggunakan frasa sandi acak. Harap ubah frasa sandi sebelum mengekspor.Basis data obrolan dieksporFrasa sandi saat ini…
@@ -1685,7 +1685,7 @@
Berkas dan mediaEnkripsi basis data?Versi basis data tidak kompatibel
- UNTUK KONSOL
+ Untuk konsolGrup sudah ada!Masukkan frasa sandiAktifkan hapus pesan otomatis?
@@ -1703,7 +1703,7 @@
Gagal verifikasi frasa sandi:Gagal hubungkan ulang serverGagal hubungkan ulang server
- EKSPERIMENTAL
+ EksperimentalEkspor basis dataImpor basis dataGagal hentikan obrolan
@@ -1830,7 +1830,7 @@
%s diunduhPesan diterimaCatatan diperbarui pada
- PESAN DAN BERKAS
+ Pesan dan berkasTema profilGambar profilHarap masukkan frasa sandi saat ini yang benar.
@@ -1908,7 +1908,7 @@
SimpanJadikan profil pribadi!Ponsel jarak jauh
- JALANKAN OBROLAN
+ Jalankan obrolanHarap simpan frasa sandi dengan aman, Anda TIDAK akan dapat mengakses obrolan jika hilang.dihapus %1$sDikirim pada: %s
@@ -1979,7 +1979,7 @@
Server baruInfo antrian pesanBentuk pesan
- ROUTING PESAN PRIBADI
+ Routing pesan pribadiinfo antrean server: %1$s\n\npesan terakhir diterima: %2$sHanya data profil lokalBuka perubahan
@@ -2024,7 +2024,7 @@
Profil, kontak, dan pesan terkirim Anda disimpan di perangkat Anda.Platform perpesanan dan aplikasi yang melindungi privasi dan keamanan Anda.Untuk melindungi privasi Anda, SimpleX gunakan ID terpisah untuk setiap kontak.
- PROXY SOCKS
+ Proxy SOCKSTingkatkan dan buka obrolanKetuk untuk gabung ke samaranAnda memblokir %s
@@ -2423,7 +2423,7 @@
Chat dengan anggota sebelum mereka bergabung.HubungkanTerhubung lebih cepat! 🚀
- PERMINTAAN KONTAK DARI GRUP
+ Permintaan kontak dari grupkontak harus menerima…Buat alamat AndaOpsi tidak berlaku
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml
index adce58e804..c3f3461c48 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml
@@ -269,7 +269,7 @@
L\'archivio chiavi di Android è usato per memorizzare in modo sicuro la password; permette il funzionamento del servizio di notifica.Permetti ai tuoi contatti di inviare messaggi vocali.Database della chat eliminato
- ICONA APP
+ Icona appIdeale per la batteria. Riceverai notifiche solo quando l\'app è in esecuzione (NESSUN servizio in secondo piano).]]>Consuma più batteria! L\'app funziona sempre in secondo piano: le notifiche vengono mostrate istantaneamente.]]>chiamata…
@@ -378,24 +378,24 @@
Attiva le chiamate dalla schermata di blocco tramite le impostazioni.Fotocamera frontale/posterioreRiaggancia
- CHIAMATE
- DATABASE DELLA CHAT
+ Chiamate
+ Database della chatDatabase della chat importatoChat in esecuzione
- CHAT
+ ChatIl database è crittografato con una password casuale. Cambiala prima di esportare.Password del databaseEliminare il profilo di chat\?Elimina databaseStrumenti di sviluppo
- DISPOSITIVO
+ DispositivoErrore nell\'eliminazione del database della chatErrore nell\'esportazione del database della chatErrore nell\'avvio della chatErrore nell\'interruzione della chatFunzionalità sperimentaliEsporta database
- AIUTO
+ AiutoChat fermataErrore del databaseLa password del database è diversa da quella salvata nell\'archivio chiavi.
@@ -437,7 +437,7 @@
Errore nella creazione del link del gruppoErrore nell\'eliminazione del link del gruppoEspandi la selezione dei ruoli
- PER CONSOLE
+ Per consoleLink del gruppoIl gruppo verrà eliminato per tutti i membri. Non è reversibile!Il gruppo verrà eliminato per te. Non è reversibile!
@@ -697,23 +697,23 @@
Importare il database della chat\?Importa databaseModalità incognito
- MESSAGGI E FILE
+ Messaggi e fileNuovo archivio databaseVecchio archivio del databaseRiavvia l\'app per creare un profilo di chat nuovo.Riavvia l\'app per usare il database della chat importato.
- AVVIA CHAT
+ Avvia chatInvia le anteprime dei linkImposta la password per esportare
- IMPOSTAZIONI
- PROXY SOCKS
+ Impostazioni
+ Proxy SOCKSFermaFermare la chat\?Ferma la chat per esportare, importare o eliminare il database della chat. Non potrai ricevere e inviare messaggi mentre la chat è ferma.
- SUPPORTA SIMPLEX CHAT
- TEMI
+ Supporta SimpleX Chat
+ TemiQuesta azione non può essere annullata: il tuo profilo, i contatti, i messaggi e i file andranno persi in modo irreversibile.
- TU
+ TuIl tuo database della chatIl tuo attuale database di chat verrà ELIMINATO e SOSTITUITO con quello importato.
\nQuesta azione non può essere annullata: il tuo profilo, i contatti, i messaggi e i file andranno persi in modo irreversibile.
@@ -774,7 +774,7 @@
Invita al gruppoEsci dal gruppoNome locale
- MEMBRO
+ MembroIl membro verrà rimosso dal gruppo, non è reversibile!Nuovo ruolo del membroNessun contatto selezionato
@@ -806,7 +806,7 @@
Salva il profilo del grupposecInvio tramite
- SERVER
+ ServerCambia indirizzo di ricezioneSistemaScadenza connessione TCP
@@ -994,7 +994,7 @@
Conferma aggiornamenti databasemigrazione diversa nell\'app/nel database: %s / %sConferma di migrazione non valida
- SPERIMENTALE
+ SperimentaleL\'immagine verrà ricevuta quando il tuo contatto completerà l\'invio.la versione del database è più recente di quella dell\'app, ma nessuna migrazione downgrade per: %sIl file verrà ricevuto quando il tuo contatto completerà l\'invio.
@@ -1102,7 +1102,7 @@
Per connettervi, il tuo contatto può scansionare il codice QR o usare il link nell\'app.Quando le persone chiedono di connettersi, puoi accettare o rifiutare.Indirizzo SimpleX
- COLORI DELL\'INTERFACCIA
+ Colori dell\'interfacciaI tuoi contatti resteranno connessi.Aggiungi l\'indirizzo al tuo profilo, in modo che i tuoi contatti di SimpleX possano condividerlo con altre persone. L\'aggiornamento del profilo verrà inviato ai tuoi contatti di SimpleX.Crea un indirizzo per consentire alle persone di connettersi con te.
@@ -1230,7 +1230,7 @@
nessun testoSi sono verificati alcuni errori non fatali durante l\'importazione:Riavvia
- APP
+ AppLe notifiche smetteranno di funzionare fino a quando non riavvierai l\'appSpegniSpegnere\?
@@ -1261,7 +1261,7 @@
L\'invio delle ricevute di consegna sarà attivo per tutti i contatti.Errore nell\'attivazione delle ricevute di consegna!Puoi attivarle più tardi nelle impostazioni
- INVIA RICEVUTE DI CONSEGNA A
+ Invia ricevute di consegna aconcordando la crittografia per %s…Ricevute di consegna!Contatti
@@ -1779,7 +1779,7 @@
NON inviare messaggi direttamente, anche se il tuo server o quello di destinazione non supporta l\'instradamento privato.NON usare l\'instradamento privato.No
- INSTRADAMENTO PRIVATO DEI MESSAGGI
+ Instradamento privato dei messaggiInvia messaggi direttamente quando l\'indirizzo IP è protetto e il tuo server o quello di destinazione non supporta l\'instradamento privato.Per proteggere il tuo indirizzo IP, l\'instradamento privato usa i tuoi server SMP per consegnare i messaggi.Non protetto
@@ -1787,7 +1787,7 @@
Proteggi l\'indirizzo IPL\'app chiederà di confermare i download da server di file sconosciuti (eccetto .onion o quando il proxy SOCKS è attivo).Senza Tor o VPN, il tuo indirizzo IP sarà visibile ai server di file.
- FILE
+ FileSenza Tor o VPN, il tuo indirizzo IP sarà visibile a questi relay XFTP:
\n%1$s.Tema della chat
@@ -2056,7 +2056,7 @@
Errore nel cambio di profiloSeleziona il profilo di chatCondividi il profilo
- DATABASE DELLA CHAT
+ Database della chatModalità di sistemaRimuovere l\'archivio?I messaggi verranno eliminati. Non è reversibile!
@@ -2448,7 +2448,7 @@
Errore nel rifiuto della richiesta di contattoEntra nel gruppoApri la chat
- Apri una chat nuova
+ Apri la nuova chatApri il nuovo gruppoApri per accettareApri per connettere
@@ -2512,7 +2512,7 @@
Condividi il link vecchioIl link sarà breve e il profilo del gruppo verrà condiviso attraverso il link.Aggiorna il link del gruppo
- RICHIESTE DI CONTATTO DAI GRUPPI
+ Richieste di contatto dai gruppiIl membro è eliminato - impossibile accettare la richiestaconnessione richiesta dal gruppo %1$sQuesta impostazione è per il tuo profilo attuale
@@ -2555,7 +2555,7 @@
VideoMessaggi vocaliFiltro
- CONNESSIONE FALLITA
+ Connessione fallitafallitoSe sei dentro canali o ne hai creati, essi smetteranno di funzionare definitivamente.%1$d/%2$d relay attivo/i
@@ -2578,7 +2578,7 @@
Nome del canaleIl canale verrà eliminato per tutti gli iscritti, non è reversibile!Il canale verrà eliminato per te, non è reversibile!
- Il canale sarà operativo con %1$d di %2$d relay. Procedere?
+ Il canale sarà operativo con %1$d di %2$d relay. Continuare?Relay di chatRelay di chatRelay di chat
@@ -2619,13 +2619,13 @@
Nessun relay di chat attivato.Non tutti i relay sono connessiApri canale
- Apri un canale nuovo
- PROPRIETARIO
+ Apri il nuovo canale
+ ProprietarioProprietariIndirizzo relay preimpostatoNome relay preimpostatorelay
- RELAY
+ RelayIndirizzo del relayIndirizzo del relayConnessione del relay fallita
@@ -2636,7 +2636,7 @@
Il server richiede l\'autorizzazione per connettersi al relay, controlla la password.Avviso del serverCondividi l\'indirizzo del relay
- ISCRITTO
+ IscrittoIscrittiGli iscritti usano il link del relay per connettersi al canale.\nL\'indirizzo del relay è stato usato per impostare questo relay per il canale.L\'iscritto verrà rimosso dal canale, non è reversibile!
@@ -2686,7 +2686,7 @@
%1$d relay falliti%1$d relay non attivi%1$d relay rimossi
- L\'aggiunta di relay verrà supportata prossimamente.
+ Aggiungi relay per ripristinare la consegna dei messaggi.Tutti i relay fallitiTutti i relay rimossiimpossibile trasmettere
@@ -2802,4 +2802,38 @@
Barra inferioreL\'anteprima del link verrà richiesta via proxy SOCKS. La ricerca DNS può ancora accadere localmente tramite il tuo risolutore DNS.Barra superiore
+ Il tuo nuovo canale %1$s è connesso a %2$d di %3$d relay.\nSe annulli, il canale verrà eliminato. Potrai crearlo di nuovo.
+ Aggiungi
+ Aggiungi relay
+ Aggiungi relay
+ Annulla ed elimina il canale
+ %d relay selezionato/i
+ Errore di aggiunta dei relay
+ Nessun relay disponibile
+ Nessun relay
+ Nessun relay selezionato
+ Relay aggiunti: %1$s.
+ Il relay verrà rimosso dal canale, non è reversibile!
+ rimosso
+ Rimuovi relay
+ Rimuovere il relay?
+ Seleziona i relay
+ Questo è l\'ultimo relay attivo. La sua rimozione impedirà la consegna dei messaggi agli iscritti.
+ Chiudi l\'app
+ Errore di eliminazione del messaggio
+ Dalla cronologia
+ Se scegli Chiudi, i messaggi non verranno ricevuti.\nPuoi cambiarlo più tardi nelle impostazioni di Aspetto.
+ Tieni SimpleX attivo in secondo piano per ricevere i messaggi.
+ Riduci nell\'area delle notifiche
+ Ridurre nell\'area delle notifiche?
+ Riduci nell\'area delle notifiche alla chiusura della finestra
+ Esci da SimpleX
+ Mostra SimpleX
+ SimpleX
+ SimpleX — %d non letto/i
+ Un\'altra istanza dell\'app potrebbe essere in esecuzione o non si è chiusa correttamente. Avviare comunque?
+ L\'app è già in esecuzione
+ rifiutato
+ rifiutato dall\'operatore del relay
+ Stato
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml
index faf69dfd03..61613e63b0 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml
@@ -973,7 +973,7 @@
רמקול כבוירמקול פעילהגדרות
- תמיכה ב־SIMPLEX CHAT
+ תמיכה ב־SimpleX Chatלעצור צ׳אט\?עיצרו את הצ׳אט כדי לייצא, לייבא או למחוק את מסד הנתונים. לא תוכלו לקבל ולשלוח הודעות בזמן שהצ׳אט מופסק.עצור
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml
index 5c17946c24..9784befef6 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml
@@ -880,7 +880,7 @@
メールを送るメディア共有…SimpleXリンク
- SIMPLEX CHATを支援
+ SimpleX Chatを支援テストサーバ受信アドレスは別のサーバーに変更されます。アドレス変更は送信者がオンラインになった後に完了します。あなたのプライバシーを守るために、他のアプリと違って、ユーザーIDの変わりに SimpleX メッセージ束毎にIDを配布し、各連絡先が別々と扱います。
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml
index 83f937db32..a60dea56b7 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml
@@ -822,7 +822,7 @@
설정링크 미리보기 보내기SOCKS 프록시
- SIMPLEX CHAT 도와주기
+ SimpleX Chat 도와주기나실험적 기능표시 :
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ku/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ku/strings.xml
index 92985b15be..09c428e48b 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/ku/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/ku/strings.xml
@@ -282,13 +282,13 @@
Adresa serverêAdresAdresa serverê li eyarên torê nayê.
- SERVER
+ ServerMelûmata serveranCeribandina serverê bi ser neket!Versiyona serverê li eyarên torê nayê.1 roj deyneTercihên komê diyar bike
- EYAR
+ EyarParve bikeLînka 1-carê parve bikeAdresê parve bike
@@ -337,7 +337,7 @@
xet/xêz/xîşkBiqewetAbonekirî
- PIŞT BIDE SIMPLEX CHATÊ
+ Pişt bide SimpleX ChatêBiguhereSîstemSîstem
@@ -513,7 +513,7 @@
Kompîter ne aktîv eGirêdana bi kompîterê re qut bûDetay
- CIHAZ
+ Cihaz%d dosya bi mezibnbûniya timam ya %s%d hewadîsên komê%d seet
@@ -588,7 +588,7 @@
Tu dihêlîte ev endam qebûl kirtu: %1$s
- TU
+ TutuErêerê
@@ -633,11 +633,11 @@
Lînkê vekeLînka timam vekeLînka paqij veke
- ARÎKARÎ
- APLÎKASYON
- DOSYA
+ Arîkarî
+ Aplîkasyon
+ DosyaJi nû ve veke
- PROKSIYA SOCKSÊ
+ Proksiya SOCKSêSûretên profîlanGirêdana torêJi kompîterê bişuxulîne
@@ -687,7 +687,7 @@
Ji admîn blokkirîblokkirîne aktîv
- ENDAM
+ EndamRolKomTe standin bi riya
@@ -785,10 +785,10 @@
Profîla siḧbetêTu siḧbeta xwe qontrol dikî!Siḧbetê bişuxulîne
- SIḦBET
+ SiḧbetRengên siḧbetêSiḧbet sekinandî ye
- DATABASA SIḦBETÊ
+ Databasa siḧbetêBer siḧbet were sekinandin?Xeletî di sekinandina siḧbetê deBer profîla siḧbetê were jêbirin?
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml
index bccd49eed9..950569c85b 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml
@@ -17,10 +17,10 @@
Skambutis jau baigtas!AtsilieptiSkambutis baigtas
- SKAMBUČIAI
+ SkambučiaiLeisti jūsų kontaktams negrįžtamai ištrinti išsiųstas žinutes. (24 valandas)Atgal
- PROGRAMĖLĖS PIKTOGRAMA
+ Programėlės piktogramavisadaLeisti jūsų kontaktams siųsti balso žinutes.Leisti negrįžtamą žinučių ištrynimą tik tuo atveju, jei jūsų kontaktas jums tai leidžia. (24 valandas)
@@ -78,8 +78,8 @@
Apversti kamerąAtmestas skambutisPrivatumas ir saugumas
- ĮRENGINYS
- PAGALBA
+ Įrenginys
+ PagalbaŠifruotiŠalintiIštrinti grupę
@@ -241,7 +241,7 @@
Išjungti garsiakalbįĮjungti garsiakalbįPraleistos žinutės
- NUSTATYMAI
+ NustatymaiSistemosnežinomas žinutės formatasSimpleX kontakto adresas
@@ -292,7 +292,7 @@
Išjungti vaizdąĮjungti vaizdąJūsų privatumas
- JŪS
+ JūsNeteisinga slaptafrazė!SimpleXjūs
@@ -348,10 +348,10 @@
Įrašyti ir pranešti grupės nariamsgautas patvirtinimas…Praleistas skambutis
- POKALBIAI
- APIPAVIDALINIMAI
+ Pokalbiai
+ ApipavidalinimaiInkognito veiksena
- ŽINUTĖS IR FAILAI
+ Žinutės ir failaiNorėdami naudoti importuotą pokalbio duomenų bazę, paleiskite programėlę iš naujo.Pakviesti nariusIšnykstančios žinutės šiame pokalbyje yra uždraustos.
@@ -420,7 +420,7 @@
Pokalbio profilisProfilis yra bendrinamas tik su jūsų kontaktais.„GitHub“ saugykloje.]]>
- SOCKS ĮGALIOTASIS SERVERIS
+ SOCKS įgaliotasis serverisĮrašyti slaptafrazę ir atverti pokalbįAtkurti atsarginę duomenų bazės kopijąAtkurti atsarginę duomenų bazės kopiją\?
@@ -459,7 +459,7 @@
Kontakto nuostatosPrisijungtiKeisti
- SERVERIAI
+ ServeriaiIšvalytiNebeslėpti profilioPer daug vaizdo įrašų!
@@ -545,7 +545,7 @@
Išjungti garsąVisi programėlės duomenys bus ištrinti.Sukuriamas tuščias pokalbių profilis nurodytu pavadinimu ir programėlė atveriama kaip įprasta.
- PROGRAMĖLĖ
+ ProgramėlėSaugiam slaptafrazės saugojimui yra naudojama „Android Keystore“ – tai įgalina pranešimų tarnybą veikti.Papildoma antrinė spalvaPapildomas akcentavimas
@@ -636,7 +636,7 @@
Sukurti grupę: sukurti naują grupę.]]>Pridėti kontaktąTinkinti apipavidalinimą
- POKALBIO DUOMENŲ BAZĖ
+ Pokalbio duomenų bazėNaudotojo sąsaja kinų ir ispanų kalbomisPranešimai apie pristatymą!Išjungti SimpleX užraktą
@@ -855,7 +855,7 @@
Prisijungti inkognito režimuReikalinga slaptafrazėUždrausti siųsti balso žinutes.
- EKSPERIMENTINIS
+ EksperimentinisGreitai ir nelaukiant kol siuntėjas prisijungs!Failai ir medijaFailai ir medija yra draudžiami šioje grupėje.
@@ -1000,7 +1000,7 @@
ištrintas kontaktaspakviestasIštrinta
- KONSOLEI
+ KonsoleiBlokuotiProtokolui skirtas laikasnumatyta (%s)
@@ -1106,7 +1106,7 @@
Prisijungimas, kurį priėmėte, bus atšauktas!Bakstelėkite, kad įklijuoti nuorodąTestuoti serverį
- TEMOS SPALVOS
+ Temos spalvosRodyti lėtus API iškvietimusNustoti bendrinti adresą?Žinučių siuntimo ir programų platforma, apsauganti jūsų privatumą ir saugumą.
@@ -1217,7 +1217,7 @@
Nustatyti duomenų slaptafrazęNustatyti slaptafrazęRodyti paskutines žinutes
- PALAIKYKITE SIMPLEX CHAT
+ Palaikykite SimpleX ChatJų galima nepaisyti kontaktų ir grupių nustatymuose.Šis veiksmas negali būti atšauktas - žinutės išsiųstos ir gautos anksčiau nei pasirinkta bus ištrintos. Tai gali užtrukti kelias minutes.%s, %s ir %d kiti nariai prisijungė
@@ -1516,7 +1516,7 @@
paslaptisPranešimai nustos veikti iki tol kol paleisite programėlę iš naujoGalite naudoti markdown, kad formatuoti žinutes:
- PALEISTI POKALBIUS
+ Paleisti pokalbiusNaudoti iš darbastalioSveikinimo žinutė yra per ilgaŽinučių reakcijos
@@ -1618,7 +1618,7 @@
Galite paleisti pokalbius per programėlės nustatymus/ duomenų bazę arba paleisdami programėlę iš naujo.pašalino jusModeruota
- NARYS
+ Narys%s %snėra tekstoMeniu ir įspėjimai
@@ -1651,7 +1651,7 @@
Naudoti atsiktinę slaptafrazęlygiaverčiai mazgaiPašalinti slaptafrazę iš nustatymų?
- SIŲSTI PRISTATYMO KVITUS PAS
+ Siųsti pristatymo kvitus pasPristatymo kvitai yra išjungti %d grupėmsTurite naudoti pačią naujausią pokalbių duomenų bazės versiją TIK viename įrenginyje, kitaip galite nebegauti žinučių iš kai kurių kontaktų.Nauja slaptafrazė…
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nb-rNO/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nb-rNO/strings.xml
index a6385a5ce0..1275c31573 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/nb-rNO/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/nb-rNO/strings.xml
@@ -124,7 +124,7 @@
En annen grunnSvar anropHvem som helst kan være vert for servere.
- APP
+ AppAppen kjører alltid i bakgrunnenApp build: %sAppen kan bare motta varsler når den er åpen, ingen bakgrunnstjeneste vil bli startet.
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml
index cc81e5365b..53ff325f43 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml
@@ -5,7 +5,7 @@
Oproepen op vergrendelscherm:oproep bezigGesprek bezig
- OPROEPEN
+ OproepenAnnulerenBestandsvoorbeeld annulerenAnnuleer afbeeldingsvoorbeeld
@@ -23,7 +23,7 @@
Sta toe om spraak berichten te verzenden.Chat is actiefWissen
- CHAT DATABASE
+ Chat databaseChat consoleChat database geïmporteerdChat database verwijderd
@@ -85,7 +85,7 @@
App build: %sApp kan alleen meldingen ontvangen wanneer deze actief is, er wordt geen achtergrondservice gestartUiterlijk
- APP ICON
+ App iconApp versieApp versie: v%s voor elk chatprofiel dat je in de app hebt .]]>
@@ -119,7 +119,7 @@
Chat is gestoptChat voorkeurenChatprofiel
- CHATS
+ ChatsPraat met de ontwikkelaarsControleer het server adres en probeer het opnieuw.Bestand
@@ -231,7 +231,7 @@
Verwijderen voor iedereenLink verwijderendirect
- APPARAAT
+ ApparaatVerwijder alle bestandenBerichten verwijderen naDirecte berichten
@@ -309,7 +309,7 @@
e2e versleuteld video gesprekSchakel oproepen vanaf het vergrendelscherm in via Instellingen.Ophangen
- HELP
+ HelpExperimentele functiesFout bij het starten van de chatDatabase exporteren
@@ -358,7 +358,7 @@
Fout bij het accepteren van een contactverzoekGroep uitnodiging verlopenBestand
- VOOR CONSOLE
+ Voor consoleGroep profiel wordt opgeslagen op de apparaten van de leden, niet op de servers.VerborgenDe groep wordt voor u verwijderd, dit kan niet ongedaan worden gemaakt!
@@ -473,8 +473,8 @@
VerlatenLidlink voorbeeld afbeelding
- LID
- BERICHTEN EN BESTANDEN
+ Lid
+ Berichten en bestandenOpenen in mobiele app en tik vervolgens op Verbinden in de app.]]>Lid wordt uit de groep verwijderd, dit kan niet ongedaan worden gemaakt!Fout bij bezorging van bericht
@@ -730,10 +730,10 @@
App scherm verbergenUw privacyLink voorbeelden verzenden
- INSTELLINGEN
- ONDERSTEUNING SIMPLEX CHAT
- JIJ
- CHAT UITVOEREN
+ Instellingen
+ Ondersteuning SimpleX Chat
+ Jij
+ Chat uitvoerenUw chat databaseWachtwoord instellen om te exporterenStart de app opnieuw om een nieuw chatprofiel aan te maken.
@@ -790,7 +790,7 @@
Direct bericht sturenDe rol wordt gewijzigd in "%s". De gebruiker ontvangt een nieuwe uitnodiging.Verzenden via
- SERVERS
+ ServersResetten naar standaardwaardenOntvangst adres wijzigenProtocol timeout
@@ -874,11 +874,11 @@
Bericht delen…SimpleX VergrendelenSla het wachtwoord op in Keychain
- SOCKS PROXY
+ SOCKS proxyDank aan de gebruikers – draag bij via Weblate!De app haalt regelmatig nieuwe berichten op - het gebruikt een paar procent van de batterij per dag. De app maakt geen gebruik van push meldingen, gegevens van uw apparaat worden niet naar de servers verzonden.De afbeelding kan niet worden gedecodeerd. Probeer een andere afbeelding of neem contact op met de ontwikkelaars.
- THEMA\'S
+ Thema\'sScan server QR-codeDeze string is geen verbinding link!Deze actie kan niet ongedaan worden gemaakt, de berichten die eerder zijn verzonden en ontvangen dan geselecteerd, worden verwijderd. Het kan enkele minuten duren.
@@ -994,7 +994,7 @@
Database-ID\'s en Transport isolatie optie.Verbergen:Ontwikkelaars opties tonen
- EXPERIMENTEEL
+ ExperimenteelVerwijder profielProfiel wachtwoordChatprofiel zichtbaar maken
@@ -1146,7 +1146,7 @@
Zorg ervoor dat het bestand de juiste YAML-syntaxis heeft. Exporteer het thema om een voorbeeld te hebben van de themabestandsstructuur.Database openen…Gebruikershandleiding.]]>
- INTERFACE KLEUREN
+ Interface kleurenU kunt uw adres delen als een link of QR-code - iedereen kan verbinding met u maken.Alle app-gegevens worden verwijderd.Er wordt een leeg chatprofiel met de opgegeven naam gemaakt en de app wordt zoals gewoonlijk geopend.
@@ -1226,7 +1226,7 @@
geen tekstEr zijn enkele niet-fatale fouten opgetreden tijdens het importeren:Afsluiten\?
- APP
+ AppHerstartenAfsluitenMeldingen werken niet meer totdat u de app opnieuw start
@@ -1285,7 +1285,7 @@
Inschakelen (overschrijvingen behouden)Het verzenden van ontvangst bevestiging is uitgeschakeld voor %d-contactpersonenUitschakelen voor iedereen
- STUUR ONTVANGST BEVESTIGING NAAR
+ Stuur ontvangst bevestiging naarOntvangst bevestiging verzendenDe tweede vink die we gemist hebben! ✅Filter ongelezen en favoriete chats.
@@ -1779,13 +1779,13 @@
Om uw IP-adres te beschermen, gebruikt privéroutering uw SMP-servers om berichten te bezorgen.Stuur GEEN berichten rechtstreeks, zelfs als uw of de bestemmingsserver geen privéroutering ondersteunt.Terugval op berichtroutering
- PRIVÉBERICHT ROUTING
+ Privébericht routingStuur berichten rechtstreeks als het IP-adres beschermd is en uw of bestemmingsserver geen privéroutering ondersteunt.Onbekende servers!Zonder Tor of VPN is uw IP-adres zichtbaar voor deze XFTP-relays:
\n%1$s.Zonder Tor of VPN is uw IP-adres zichtbaar voor bestandsservers.
- BESTANDEN
+ BestandenBescherm het IP-adresDe app vraagt om downloads van onbekende bestandsservers te bevestigen (behalve .onion of wanneer SOCKS-proxy is ingeschakeld).Fout bij het initialiseren van WebView. Update uw systeem naar de nieuwe versie. Neem contact op met ontwikkelaars.
@@ -2055,7 +2055,7 @@
Selecteer chatprofielProfiel delenUw verbinding is verplaatst naar %s, maar er is een onverwachte fout opgetreden tijdens het omleiden naar het profiel.
- CHAT DATABASE
+ Chat databaseSysteemmodusArchief verwijderen?Berichten worden verwijderd. Dit kan niet ongedaan worden gemaakt!
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml
index 9cc43851d6..51bfacb404 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml
@@ -495,36 +495,36 @@
Pominięte wiadomościTwoja prywatnośćKopia zapasowa danych aplikacji
- IKONA APLIKACJI
+ Ikona aplikacjiAutomatyczne akceptowanie obrazów
- POŁĄCZENIA
- BAZA DANYCH CZATU
+ Połączenia
+ Baza danych czatuCzat jest uruchomionyCzat jest zatrzymany
- CZATY
+ CzatyHasło do bazy danychUsuń bazę danychNarzędzia deweloperskie
- URZĄDZENIE
+ UrządzenieBłąd uruchamiania czatu
- EKSPERYMENTALNE
+ EksperymentalneFunkcje eksperymentalneEksportuj bazę danych
- POMOC
+ PomocImportuj bazę danychTryb incognito
- WIADOMOŚCI I PLIKI
+ Wiadomości i plikiNowe archiwum bazy danychStare archiwum bazy danychChroń ekran aplikacji
- URUCHOM CZAT
+ Uruchom czatWyślij podgląd linku
- USTAWIENIA
- PROXY SOCKS
+ Ustawienia
+ Proxy SOCKSZatrzymać czat\?
- WSPIERAJ SIMPLEX CHAT
- MOTYWY
- TY
+ Wspieraj SimpleX Chat
+ Motywy
+ TyTwoja baza danych czatuUstaw hasło do eksportuZatrzymaj
@@ -707,12 +707,12 @@
Błąd tworzenia linku grupyBłąd usuwania linku grupyBłąd usuwania członka
- DLA KONSOLI
+ Dla konsoliGrupaWprowadź nazwę grupy:Pełna nazwa grupy:Nazwa lokalna
- CZŁONEK
+ CzłonekCzłonek zostanie usunięty z grupy - nie można tego cofnąć!Status sieciTylko właściciele grup mogą zmieniać preferencje grupy.
@@ -724,7 +724,7 @@
Zapisać wiadomość powitalną\?Wyślij wiadomość bezpośredniąWysyłanie przez
- SERWERY
+ SerweryPrzełączZmień adres odbioruW pełni zdecentralizowana – widoczna tylko dla członków.
@@ -1136,7 +1136,7 @@
Kiedy ludzie proszą o połączenie, możesz je zaakceptować lub odrzucić.Nie stracisz kontaktów, jeśli później usuniesz swój adres.Dostosuj motyw
- KOLORY INTERFEJSU
+ Kolory interfejsuTwoje kontakty pozostaną połączone.Dodaj adres do swojego profilu, aby Twoje kontakty mogły go udostępnić innym osobom. Aktualizacja profilu zostanie wysłana do Twoich kontaktów.Utwórz adres, aby ludzie mogli się z Tobą połączyć.
@@ -1229,7 +1229,7 @@
brak tekstuPodczas importu wystąpiły niekrytyczne błędy:Restart
- APLIKACJA
+ AplikacjaPowiadomienia przestaną działać do momentu ponownego uruchomienia aplikacji.WyłączenieWyłączyć\?
@@ -1262,7 +1262,7 @@
Spraw, aby jedna wiadomość zniknęłaRenegocjuj szyfrowaniekod bezpieczeństwa zmieniony
- WYŚLIJ POTWIERDZENIA DOSTAWY DO
+ Wyślij potwierdzenia dostawy doWysyłanie potwierdzeń dostawy zostanie włączone dla wszystkich kontaktów we wszystkich widocznych profilach czatu.KontaktyWłączyć potwierdzenia\?
@@ -1772,7 +1772,7 @@
NieGdy IP ukrytyPokaż status wiadomości
- TRASOWANIE PRYWATNYCH WIADOMOŚCI
+ Trasowanie prywatnych wiadomościNIE wysyłaj wiadomości bezpośrednio, nawet jeśli serwer docelowy nie obsługuje prywatnego trasowania.Aby chronić Twój adres IP, prywatne trasowanie używa Twoich serwerów SMP, aby dostarczyć wiadomości.Nieznane serwery
@@ -1788,7 +1788,7 @@
Chroń adres IPAplikacja będzie prosić o potwierdzenie pobierań z nieznanych serwerów plików (z wyjątkiem .onion lub gdy proxy SOCKS jest włączone).Bez Tor lub VPN, Twój adres IP będzie widoczny do serwerów plików.
- PLIKI
+ PlikiMotyw profiluPokaż listę czatów w nowym oknieKolory ciemnego trybu
@@ -2065,7 +2065,7 @@
%1$d plik(ów/i) dalej są pobierane.%1$d plik(ów/i) nie udało się pobrać.Błąd zmiany profilu
- BAZA CZATU
+ Baza czatu%1$d błędów plików:\n%2$s%1$d innych błędów plików.Wiadomości zostały usunięte po wybraniu ich.
@@ -2233,7 +2233,7 @@
kontakt usuniętykontakt wyłączonykontakt nie gotowy
- PROŚBY O KONTAKT OD GRUP
+ Prośby o kontakt od grupkontakt powinien zaakceptować…Stwórz swój adres%d czat(y)
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml
index c129d68521..2d33731ce1 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml
@@ -74,16 +74,16 @@
para cada perfil de bate-papo que você tiver no aplicativo.]]>Melhor para bateria. Você receberá notificações apenas quando o aplicativo estiver em execução (SEM o serviço em segundo plano).]]>Consome mais bateria! O aplicativo em segundo plano está sempre em execução - as notificações são exibidas instantaneamente.]]>
- BATE-PAPOS
- ÍCONE DO APLICATIVO
- BANCO DE DADOS DE BATE-PAPO
+ Bate-papos
+ Ícone do aplicativo
+ Banco de dados de bate-papoO bate-papo está em execuçãoO bate-papo está paradoAlterar senha do banco de dados\?endereço alterado para vocêVocê e seu contato podem enviar mensagens temporárias.Backup de dados do aplicativo
- CHAMADAS
+ ChamadasAceitar solicitações de contato automaticamenteAparênciaO serviço em segundo plano está sempre em execução - as notificações serão exibidas assim que as mensagens estiverem disponíveis.
@@ -247,7 +247,7 @@
Tempo de conexão esgotadoExcluir mensagem do membro\?Excluir fila
- DISPOSITIVO
+ DispositivoFerramentas de desenvolvedorconectando (introduzido)Tonalidade
@@ -378,7 +378,7 @@
Arquivo salvoOs membros podem enviar mensagens de voz.O grupo será excluído para todos os membros - isso não pode ser desfeito!
- AJUDA
+ AjudaOcultar contato e mensagemComo usarComo usar markdown
@@ -420,7 +420,7 @@
Servidores ICE (um por linha)IgnorarA imagem será recebida quando seu contato estiver online, aguarde ou verifique mais tarde!
- SERVIDORES
+ ServidoresRecebendo viaStatus da conexãoseg
@@ -495,8 +495,8 @@
Usar proxy SOCKS\?Chamada rejeitadaRestaurar o backup do banco de dados
- PARA CONSOLE
- EXECUTAR BATE-PAPO
+ Para console
+ Executar bate-papoPararDefinir senha para exportarReinicie o aplicativo para usar o banco de dados do chat importado.
@@ -571,7 +571,7 @@
você mudou o cargo de %s para %sNovo cargo de membroRemover
- MEMBRO
+ MembroO membro será removido do grupo - isso não pode ser desfeito!CargoEnviando via
@@ -663,8 +663,8 @@
Interface chinesa e espanholaMaior redução no uso da bateriaMais melhorias chegarão em breve!
- VOCÊ
- MENSAGENS E ARQUIVOS
+ Você
+ Mensagens e arquivosSeu banco de dados de bate-papoVocê removeu %1$sremovido
@@ -800,7 +800,7 @@
Ocultar perfilconfirmação recebida…O servidor de relay protege seu endereço IP, mas pode observar a duração da chamada.
- EXPERIMENTAL
+ Experimentalvocê alterou o endereçoAtualização do banco de dadosO cargo será alterado para "%s". O membro receberá um novo convite.
@@ -819,7 +819,7 @@
Somente o proprietários de grupo podem ativar mensagens de vozvocê compartilhou um link de uso únicoVocê será conectado quando sua solicitação de conexão for aceita, aguarde ou verifique mais tarde!
- CONFIGURAÇÕES
+ ConfiguraçõesDefina a mensagem mostrada aos novos membros!ConfiguraçõesAlternar endereço de recebimento
@@ -848,7 +848,7 @@
LigarBem-vindo(a)!O futuro da transmissão de mensagens
- PROXY SOCKS
+ Proxy SOCKSA tentativa de alterar a senha do banco de dados não foi concluída.Pare o bate-papo para exportar, importar ou excluir o banco de dados do chat. Você não poderá receber e enviar mensagens enquanto o chat estiver interrompido.%s segundo(s)
@@ -887,7 +887,7 @@
chamada de vídeoMostrarServidores ICE WebRTC
- TEMAS
+ TemasAtualizarO app busca novas mensagens periodicamente – ele usa alguns por cento da bateria por dia. O aplicativo não usa notificações por push – os dados do seu dispositivo não são enviados para os servidores.Para receber notificações, por favor, digite a senha do banco de dados
@@ -1001,7 +1001,7 @@
desativadoDesatualizar e abrir o bate-papodesativado
- APOIE SIMPLEX CHAT
+ Apoie SimpleX ChatEsta ação não pode ser desfeita - as mensagens enviadas e recebidas antes do selecionado serão excluídas. Pode levar vários minutos.Confirme as atualizações do banco de dadosSomente o cliente dos dispositivos armazenam perfis de usuários, contatos, grupos e mensagens.
@@ -1127,7 +1127,7 @@
Você não perderá seus contatos se, posteriormente, excluir seu endereço.Endereço SimpleXQuando as pessoas solicitam uma conexão, você pode aceitá-la ou rejeitá-la.
- CORES DA INTERFACE
+ Cores da interfacecompartilhar com os contatosA atualização do perfil será enviada aos seus contatos.Salvar configurações\?
@@ -1253,7 +1253,7 @@
Correção não suportada pelo membro do grupoconcordando com criptografia…Permitir o envio de arquivos e mídia.
- APP
+ Appcriptografia OKrenegociação de criptografia necessáriacriptografia concordada para %s
@@ -1284,7 +1284,7 @@
Ativar recibos?Encontrar conversas mais rápidoContatos
- ENVIAR RECIBOS DE ENTREGA PARA
+ Enviar recibos de entrega paraEnviar confirmações está desativado para %d contatos.Enviar confirmações está ativado para %d contatos.Enviar confirmações
@@ -1862,9 +1862,9 @@
Alto falanteHeadphonesSem Tor ou VPN, seu endereço de IP ficará visível para servidores de arquivo.
- ARQUIVOS
+ ArquivosFotos de perfil
- ROTEAMENTO DE MENSAGEM PRIVADA
+ Roteamento de mensagem privadacriptografia padrão ponta a pontaproprietáriosMigrar para outro dispositivo
@@ -2055,7 +2055,7 @@
Barras de ferramentas de aplicativos acessívelFalha no baixar de %1$d arquivo(s).%1$s mensagens não encaminhadas.
- DADOS DO BATE-PAPO
+ Dados do bate-papoUtilize credenciais aleatóriasO arquivo de banco de dados enviado será removido permanentemente dos servidores.Use credenciais diferentes de proxy para cada conexão.
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml
index 5f12e762aa..af292da504 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml
@@ -28,9 +28,9 @@
Backup de dados da aplicaçãoAceitar imagens automaticamenteCódigo de acesso definido!
- VOCÊ
- MENSAGENS E FICHEIROS
- ÍCONE DA APLICAÇÃO
+ Você
+ Mensagens e ficheiros
+ Ícone da aplicação1 mêsMensagensAdicionar mensagem de boas-vindas
@@ -137,7 +137,7 @@
EliminarEliminar todos os ficheirosEliminar base de dados
- BASE DE DADOS DE CONVERSA
+ Base de dados de conversaBase de dados de conversa eliminadaNome para ExibiçãoMostrar:
@@ -184,7 +184,7 @@
Colar ligação recebidaSenha não encontrada na Keystore, por favor insira-a manualmente. Isto pode ter acontecido se você restaurou os dados da aplicação usando uma ferramenta de backup. Se não for o caso, entre em contato com os desenvolvedores.O servidor requer autorização para criar filas, verifique a senha
- SERVIDORES
+ ServidoresO servidor requer autorização para fazer upload, verifique a senhaUsar hosts .onion como Não se o proxy SOCKS não o suportar.]]>Usar hosts .onion
@@ -226,7 +226,7 @@
Chamada já finalizada!Chamada em cursoChamada finalizada
- CHAMADAS
+ ChamadasPor perfil de conversa (padrão) ou por ligação (BETA).Não é possível aceder à Keystore para salvar a senha da base de dadosNão é possível convidar o contato!
@@ -251,7 +251,7 @@
Erro ao eliminar ligação de grupoPerfil de conversaAlterar o modo de bloqueio
- CONVERSAS
+ ConversasConversa em execuçãoErro ao alterar configuraçãoAlterar a senha da base de dados\?
@@ -471,7 +471,7 @@
ID da base de dadosEliminar ficheiroEliminar contacto?
- DISPOSITIVO
+ DispositivoMensagens diretasDescentralizadomensagem duplicada
@@ -540,7 +540,7 @@
%1$d mensagens ignoradas.Importar base de dadosAs suas definições
- DEFINIÇÕES
+ DefiniçõesPartilharPartilhar endereçoDefinições
@@ -554,12 +554,12 @@
\nEsta ação é irreversível - o seu perfil, contactos, mensagens e ficheiros serão irreversivelmente perdidos.Marcar como não lidomembro
- MEMBRO
+ MembroMáximo de 40 segundos, recebido instantaneamente.MaisRede e servidoresConfigurações avançadas
- EXPERIMENTAL
+ ExperimentalVocê pode iniciar a conversa através das Definições da aplicação / Base de Dados ou reiniciando a aplicação.AtualizarA atualização das definições reconectará o cliente a todos os servidores.
@@ -572,10 +572,10 @@
Muito provavelmente este contato eliminou a conexão consigo.Este texto está disponível nas definiçõesPode ser alterado mais tarde através das definições.
- AJUDA
- SUPORTE SIMPLEX CHAT
+ Ajuda
+ Suporte SimpleX ChatFuncionalidades experimentais
- TEMAS
+ TemasEscuroTema escuronunca
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml
index 81cf8ed452..b8f8f3f111 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml
@@ -98,7 +98,7 @@
și %d alte evenimenteRăspunde la apelAndroid Keystore va fi folosit pentru a stoca în siguranță parola după ce repornești aplicația sau schimbi parola — acest lucru va permite primirea de notificări.
- APLICAȚIE
+ AplicațieCreează grupApeluri audio și videoArhivează și încarcă
@@ -109,7 +109,7 @@
Apel audioapel audioAudio oprit
- PICTOGRAMĂ APLICAȚIE
+ Pictogramă aplicațieCod de acces aplicațieCreează grup secretCreează coadă
@@ -292,11 +292,11 @@
Afișează:Afișează erorile internesecret
- SETĂRI
+ Setări%s conectatsetați o nouă poză de profilTrimis la: %s
- SERVERE
+ ServereTrimite mesaj în direct%s descărcatPartajați adresa cu contactele?
@@ -376,7 +376,7 @@
Hash mesaj incorectSchimbă adresa de primireConversația este oprită. Dacă ai folosit deja această bază de date pe alt dispozitiv, ar trebui să o transferi înapoi înainte de a porni conversația.
- APELURI
+ Apeluriv-ați schimbat rolul în %sCapacitate depășită - destinatarul nu a primit mesajele trimise anterior.Schimbă codul de acces autodistructibil
@@ -431,8 +431,8 @@
contactul are criptare e2econtactul nu are criptare e2eContacte
- CONVERSAȚII
- BAZĂ DE DATE CONVERSAȚIE
+ Conversații
+ Bază de date conversațieBaza de date a conversației a fost ștearsăConversația ruleazăBaza de date a conversațiilor tale
@@ -684,7 +684,7 @@
Verifică pentru actualizăriCreeazăEstompează media
- BAZĂ DE DATE CONVERSAȚIE
+ Bază de date conversațieConectează-te cu prietenii mai ușor.încercăriFinalizat
@@ -719,11 +719,11 @@
Setăriapel audio criptat e2eapel video criptat e2e
- DISPOZITIV
- EXPERIMENTAL
+ Dispozitiv
+ ExperimentalCripteazăerori de decriptare
- TU
+ Tunicio criptare e2ecriptat e2eApel video primit
@@ -825,7 +825,7 @@
Notificări și baterieDeschideActivați confirmarea de primire pentru grupuri?
- PENTRU CONSOLĂ
+ Pentru consolăModerat laRemediați conexiuneaProfiluri de conversație multiple
@@ -915,7 +915,7 @@
Nicio conversație în lista %s.Nimic selectatdeschis
- AJUTOR
+ AjutorDoar contactul tău poate trimite mesaje care dispar.Se importă arhivaMigrare dispozitiv
@@ -1010,7 +1010,7 @@
LuminosNuDeschizi linkul web?
- MESAJE ȘI FIȘIERE
+ Mesaje și fișieremoderatorRol inițialDoar proprietarii grupului pot modifica preferințele grupului.
@@ -1043,7 +1043,7 @@
Cum afectează bateriaActivare (păstrați suprascrierile)Activează codul de autodistrugere
- MEMBRU
+ MembruOperator de rețeaDesktop-uri conectateEroare la acceptarea membrului
@@ -1148,7 +1148,7 @@
Activați confirmarea de primire?Nume nou afișat:Instrumente pentru dezvoltatori
- FIȘIERE
+ FișiereDeschide linkurile din lista de conversațiiForma mesajuluiImportați baza de date
@@ -1393,7 +1393,7 @@
ȘtergeInstalați SimpleX Chat pentru terminalEroare la salvarea serverelor ICE
- CULORILE INTERFEȚEI
+ Culorile interfeței%d fișier(e) cu dimensiunea totală de %sFișiere și mediainvitat %1$s
@@ -1636,7 +1636,7 @@
Conexiuni de profil și serverrăspuns primit…Politica de confidențialitate și condițiile de utilizare.
- RUTAREA MESAJELOR PRIVATE
+ Rutarea mesajelor privateTe rugăm să stochezi parola în siguranță, altfel NU o vei putea schimba dacă o pierzi.Parola nu a fost găsită în Keystore. Te rugăm să o introduci manual. Acest lucru s-ar putea întâmpla dacă ai restaurat datele aplicației folosind un instrument de backup. Dacă nu este cazul, te rugăm să contactezi dezvoltatorii.Parola stocată în Keystore nu poate fi citită. Acest lucru se poate întâmpla după o actualizare a sistemului incompatibilă cu aplicația. Dacă nu este cazul, te rugăm să contactezi dezvoltatorii.
@@ -1767,7 +1767,7 @@
revizuit de administratoriOperatori de serverServerul de retransmisie protejează adresa IP, dar poate observa durata apelului.
- PORNIȚI CHATUL
+ Porniți chatulte-a eliminat%s la %sProtocolul serverului a fost modificat.
@@ -1800,7 +1800,7 @@
Selectează profilul de conversațieSalvează setările adresei SimpleXSalvează lista
- TRIMITE CONFIRMĂRI DE LIVRARE LA
+ Trimite confirmări de livrare laParola de autodistrugere a fost schimbată!Înregistrare actualizată laSelectează operatorii de rețea de utilizat.
@@ -1973,9 +1973,9 @@
Aplicația va cere să confirmați descărcările de pe servere de fișiere necunoscute (cu excepția celor .onion sau când proxy-ul SOCKS este activat).SistemAcestea pot fi ignorate în setările de contact și de grup.
- SUPORT SIMPLEX CHAT
- PROXY SOCKS
- TEME
+ Suport SimpleX Chat
+ Proxy SOCKS
+ TemeÎn timpul importului au apărut câteva erori non-fatale:Atingeți pentru a vă alăturaAtenție: este posibil să pierdeți unele date!
@@ -2430,7 +2430,7 @@
Trimis contactului tău după conectare.Actualizezi la o adresă permanentă?Mesaj de bun venit
- SOLICITĂRI DE CONTACT DE LA GRUPURI
+ Solicitări de contact de la grupuriAceastă setare este pentru profilul tău actualPartajează adresa vechePartajează linkul vechi
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml
index b5bbf47973..5804679dbd 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml
@@ -548,26 +548,26 @@
Отправлять картинки ссылокРезервная копия данных
- ВЫ
- НАСТРОЙКИ
- ПОМОЩЬ
- ПОДДЕРЖАТЬ SIMPLEX CHAT
- УСТРОЙСТВО
- ЧАТЫ
+ Вы
+ Настройки
+ Помощь
+ Поддержать SimpleX Chat
+ Устройство
+ ЧатыИнструменты разработчикаЭкспериментальные функции
- SOCKS-ПРОКСИ
- ЗНАЧОК
- ТЕМЫ
- СООБЩЕНИЯ И ФАЙЛЫ
- ЗВОНКИ
+ SOCKS-прокси
+ Значок
+ Темы
+ Сообщения и файлы
+ ЗвонкиРежим ИнкогнитоБаза данных
- ЗАПУСТИТЬ ЧАТ
+ Запустить чатЧат запущенЧат остановлен
- БАЗА ДАННЫХ
+ База данныхПароль базы данныхЭкспорт архива чатаИмпорт архива чата
@@ -767,7 +767,7 @@
Ошибка при удалении ссылки группыТолько владельцы группы могут изменять предпочтения группы.
- ДЛЯ КОНСОЛИ
+ Для консолиЛокальное имяID базы данных
@@ -775,7 +775,7 @@
Отправить сообщениеЧлен группы будет удалён - это действие нельзя отменить!Удалить
- ЧЛЕН ГРУППЫ
+ Член группыРольПоменять рольПоменять
@@ -790,7 +790,7 @@
прямоенепрямое (%1$s)
- СЕРВЕРЫ
+ СерверыПолучение черезОтправка черезСостояние сети
@@ -1084,7 +1084,7 @@
Ожидание видеоВидео будет получено когда Ваш контакт загрузит его.Скрыть:
- ЭКСПЕРИМЕНТАЛЬНЫЕ
+ ЭкспериментальныеТолько 10 видео могут быть отправлены одновременноРаскрыть профильВидео будет получено, когда Ваш контакт будет онлайн, пожалуйста, подождите или проверьте позже!
@@ -1226,7 +1226,7 @@
Запретить реакции на сообщения.Запретить реакции на сообщения.секунд
- ЦВЕТА ИНТЕРФЕЙСА
+ Цвета интерфейсаПоделиться адресом с контактами SimpleX?Обновление профиля будет отправлено Вашим SimpleX контактам.Об адресе SimpleX
@@ -1347,15 +1347,15 @@
Только владельцы группы могут разрешить файлы и медиа.Файлы и медиаВыключить\?
- ПРИЛОЖЕНИЕ
+ ПриложениеПерезапуститьВыключитьВыключить для всехВключить для всехВключить (кроме исключений)Выключить отчёты о доставке\?
- ОТПРАВКА ОТЧЁТОВ О ДОСТАВКЕ
- ЗАПРОСЫ НА СОЕДИНЕНИЕ ИЗ ГРУПП
+ Отправка отчётов о доставке
+ Запросы на соединение из группшифрование согласованошифрование согласовано для %sшифрование работает
@@ -1829,7 +1829,7 @@
Форма картинок профилейКвадрат, круг и все, что между ними.Будет включено в прямых разговорах!
- ФАЙЛЫ
+ ФайлыНовые темы чатовнетСветлая
@@ -1898,7 +1898,7 @@
Показать статус сообщенияПрямая доставка сообщенийРежим доставки сообщений
- КОНФИДЕНЦИАЛЬНАЯ ДОСТАВКА СООБЩЕНИЙ
+ Конфиденциальная доставка сообщенийЦвета чатаТема чатаТема профиля
@@ -2151,7 +2151,7 @@
Переслать сообщения…Проверьте правильность ссылки SimpleX.Ошибка ссылки
- БАЗА ДАННЫХ
+ База данныхОшибка инициализации WebView. Убедитесь, что у вас установлен WebView и его поддерживаемая архитектура - arm64.\nОшибка: %sЗвук отключенСообщения будут удалены - это нельзя отменить!
@@ -2207,7 +2207,7 @@
Посмотреть условия%s.]]>Условия будут автоматически приняты для включенных операторов: %s.
- Условия приняты: %s.
+ Условия приняты: %sВебсайт%s.]]>%s.]]>
@@ -2719,9 +2719,9 @@
Открыть каналОткрыть новый каналВладельцы
- ВЛАДЕЛЕЦ
+ Владелецрелей
- РЕЛЕЙ
+ РелейАдрес релеяАдрес релеяОшибка подключения релея
@@ -2740,7 +2740,7 @@
Поделиться адресом релеяПоделиться в чате⚠️ Ошибка проверки подписи: %s.
- ПОДПИСЧИК
+ ПодписчикПодписчикиПодписчик будет удалён из канала - это нельзя отменить!Начните разговор
@@ -2799,9 +2799,9 @@
Приложение удалило это сообщение после %1$d попыток его получить.Если Вы присоединились к каналам или создали их, они перестанут работать навсегда.Вы перестанете получать сообщения из этого канала. История чата сохранится.
- обновлён профиль канала
+ обновил профиль каналаошибка
- ОШИБКА СОЕДИНЕНИЯ
+ Ошибка соединенияЧат с админамиРазрешить членам группы общаться с админами.Запретить чаты с админами.
@@ -2858,7 +2858,7 @@
ошибкановыйВсе релеи недоступны
- Добавление релеев будет поддерживаться позже.
+ Добавить релеи чтобы восстановить доставку сообщений.Ожидает, когда владелец канала добавит релеи.через %1$sПодписчики используют ссылку релея для подключения к каналу.\nАдрес релея был использован для настройки этого релея для канала.
@@ -2874,4 +2874,9 @@
Нижнее менюКартинка ссылки будет загружена через SOCKS-прокси. DNS-запрос может быть локальным через Ваш резолвер.Верхнее меню
+ Добавить
+ Добавить релей
+ Добавить релеи
+ Отменить и удалить канал
+ Закрыть приложение
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml
index c355d8d9fb..df879fe7ff 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml
@@ -996,7 +996,7 @@
ข้อความที่ข้ามไปส่งระบบ
- สนับสนุน SIMPLEX แชท
+ สนับสนุน SimpleX Chatพร็อกซี SOCKSหยุดหยุดแชท\?
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml
index 0e9c54fb87..d56d308654 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml
@@ -50,8 +50,8 @@
Aramayı cevaplaUygulama veri yedeklemeTüm uygulama verileri silinir.
- UYGULAMA
- UYGULAMA SİMGESİ
+ Uygulama
+ Uygulama simgesi1 haftaşifreleme kabul ediliyor…yönetici
@@ -86,7 +86,7 @@
Konuştuğunuz kişinin uygulamasından güvenlik kodunu okut.WebRTC ICE sunucu adreslerinin doğru formatta olduğundan emin olun: Satırlara ayrılmış ve yinelenmemiş şekilde.Kaydet
- ARAYÜZ RENKLERİ
+ Arayüz renkleriSimpleX adres ayarlarını kaydetAyarlar kaydedilsin mi?Kaydet ve konuştuğun kişilere bildir
@@ -97,7 +97,7 @@
Ses kapalıDoğrulama iptal edildiYeniden başlat
- TEMALAR
+ Temalarİçe aktarılan konuşma veri tabanını kullanmak için uygulamayı yeniden başlat.Yeni bir konuşma profili oluşturmak için uygulamayı yeniden başlatın.Geri Yükle
@@ -184,7 +184,7 @@
silindidosya alma henüz desteklenmiyorsen
- geçersi̇z sohbet
+ geçersiz sohbetbağlantı %1$dTarayıcı ile%1$s tarafından
@@ -287,10 +287,10 @@
Ek ikincil renkÜyeyi çıkarKaldır
- ARAMALAR
- SOHBETLER
- SEN
- SOHBET VERİTABANI
+ Aramalar
+ Sohbetler
+ Sen
+ Sohbet veritabanıKaldırYanlış parola!Veritabanı yükseltmelerini onayla
@@ -300,7 +300,7 @@
bağlanılıyor (duyuruldu)sen: %1$skaldırıldı
- ÜYE
+ ÜyeÜyeler kendiliğinden yok olan mesajlar gönderebilir.Kendiliğinden yok olan mesaj gönderimini engelle.Yalnızca kişiniz sesli mesaj göndermeye izin veriyorsa sen de ver.
@@ -422,7 +422,7 @@
Eğer uygulamayı açarken tüm verileri yok eden erişim kodunu girersen:Eğer uygulamayı açarken bu erişim kodunu kullanırsan uygulama içi tüm veriler kalıcı olarak silinecektir!Erişim kodu belirle
- AYGIT
+ AygitVeri tabanı parolasıVeri tabanı, rastgele bir parola ile şifrelendi. Dışa aktarmadan önce lütfen değiştir.Dosyaları ve medyayı sil\?
@@ -600,7 +600,7 @@
ICE sonucuları kaydedilirken hata oluştuKullanıcı gizliliği güncellenirken hata oluştuGözde
- DENEYSEL
+ DeneyselDosya, sunuculardan silinecektir.Yedekleri geri yükledikten sonra şifrelemeyi onar.Fransız arayüzü
@@ -622,7 +622,7 @@
Konuşma başlatılırken hata oluştuDeneysel özelliklerVeri tabanını dışa aktar
- YARDIM
+ YardimVeri tabanını içe aktarKonuşma durdulurken hata oluştuİçe aktar
@@ -634,7 +634,7 @@
grup profili güncellendigrup silindiToplu konuşma bağlantısı güncellenirken hata oluştu
- UÇBİRİM İÇİN
+ Uçbirim içinGrup bağlantısıGrupdolaylı (%1$s)
@@ -801,7 +801,7 @@
arama sona erdi %1$sKilit ekranında aramalar:Kötü mesaj kimliği
- MESAJLAR VE DOSYALAR
+ Mesajlar ve dosyalarVeri tabanı parolasını değiştir\?Parola Keystore\'da bulunamadı, lütfen manuel olarak girin. Bu, uygulamanın verilerini bir yedekleme aracı kullanarak geri yüklediyseniz olabilir. Eğer durum böyle değilse, lütfen geliştiricilerle iletişime geçin.Ayrıl
@@ -1037,7 +1037,7 @@
Sohbeti durdurMevcut profili kullanSistem
- SIMPLEX CHAT\'İ DESTEKLE
+ SimpleX Chat\'i destekleSohbet veri tabanını dışa aktarmak, içe aktarmak veya silmek için sohbeti durdur. Sohbet durdurulduğunda mesaj alamaz ve gönderemezsiniz.MasaüstüBağlanmak için dokun
@@ -1134,7 +1134,7 @@
Bağlantı paylaşSimpleX Ekibi%s, %s ve %s bağlandı
- SOCKS VEKİLİ
+ SOCKS vekiliMasaüstür cihazlarSMP sunucularıUyumlu değil!
@@ -1217,7 +1217,7 @@
güvenlik kodu değiştirildiBluetooth desteği ve diğer iyileştirmeler.Ayarlar
- AYARLAR
+ AyarlarBağlanmak için doğrudan mesaj gönderinGüvenlik koduDaha hızlı gruplara katılma ve daha güvenilir mesajlar.
@@ -1352,7 +1352,7 @@
Yönlendirici sunucusu sadece lazım ise kullanılacak. Diğer taraf IP adresini görebilir.%s ın bağlantısı kesildi]]>Sunucuyu test et
- SUNUCULAR
+ SunucularSunucuları test etMesaj taslağıBir mesajı yok edin
@@ -1361,7 +1361,7 @@
Kişi doğrulandıRasgele parola kullanSistem yetkilendirilmesi yerine ayarla.
- SOHBETİ ÇALIŞTIR
+ Sohbeti çalıştırDirekt internet bağlantısı kullan?grup profili güncellendiOnion ana bilgisayarları bağlantı için gerekli olacaktır.
@@ -1446,7 +1446,7 @@
Yeni sohbetBir canlı mesaj gönder - bu yazdıklarını anlık olarak alıcıya(lara) güncelleyen bir mesajdırŞifre Yöneticisindeki parola silinsin mi?
- LERE GÖNDER
+ Lere gönderTakma adla bağlanHer zaman yönlendirici kullan.Kilidini aç
@@ -1778,7 +1778,7 @@
Sizin veya hedef sunucunun özel yönlendirmeyi desteklememesi durumunda bile mesajları doğrudan GÖNDERMEYİN.IP adresi korumalı olduğunda ve sizin veya hedef sunucunun özel yönlendirmeyi desteklemediği durumlarda mesajları doğrudan gönderin.Sizin veya hedef sunucunun özel yönlendirmeyi desteklemediği durumlarda mesajları doğrudan gönderin.
- GİZLİ MESAJ YÖNLENDİRME
+ Gizli mesaj yönlendirmeMesaj durumunu gösterIP adresinizi korumak için,gizli yönlendirme mesajları iletmek için SMP sunucularınızı kullanır.Korumasız
@@ -1805,7 +1805,7 @@
KaranlıkAydınlık modIP adresini koru
- DOSYALAR
+ DosyalarSohbet renkleriSığdırAlınan cevap
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml
index 4e62631dbb..b61b77ec9d 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml
@@ -33,14 +33,14 @@
Дозволити надсилати зникаючі повідомлення.прийнятий викликЗавжди використовувати реле
- ДОДАТОК
+ ДодатокДозволити надсилання приватних повідомлень учасникам.Дозволити безповоротно видаляти надіслані повідомлення. (24 години)Дозволяйте надсилати голосові повідомлення.Дозволити реакції на повідомлення.Вся інформація стирається при його введенні.Пароль для додатка
- ІКОНКА ДОДАТКУ
+ Іконка додаткуДозволити зникаючі повідомлення тільки за умови, що ваш контакт дозволяє їх.Дозвольте вашим контактам додавати реакції на повідомлення.Дозволити реакції на повідомлення тільки за умови, що ваш контакт дозволяє їх.
@@ -278,8 +278,8 @@
Повернути камеруВідхилений виклик%1$d пропущено повідомлень
- ЧАТИ
- SOCKS-ПРОКСІ
+ Чати
+ SOCKS-проксіПомилка при запуску чатуЗупинитиІмпортувати
@@ -416,9 +416,9 @@
Підключення викликуКонфіденційність і безпекаКонфіденційність
- НАЛАШТУВАННЯ
- ДОПОМОГА
- ПІДТРИМАЙТЕ SIMPLEX CHAT
+ Налаштування
+ Допомога
+ Підтримайте SimpleX ChatЗупиніть чат, щоб експортувати, імпортувати або видалити базу даних чату. Ви не зможете отримувати та надсилати повідомлення, поки чат зупинено.Помилка видалення бази даних чатуСповіщення будуть доставлятися лише до зупинки додатка!
@@ -464,7 +464,7 @@
ПерезапуститиБаза даних чатуЧат зупинено
- БАЗА ДАНИХ ЧАТУ
+ База даних чатуНовий архів бази данихЗупинити чат\?Ваша поточна база даних чату буде ВИДАЛЕНА та ЗАМІНЕНА імпортованою.
@@ -658,11 +658,11 @@
Пароль самознищення увімкнено!Пароль самознищення змінено!Ваш профіль, контакти та доставлені повідомлення зберігаються на вашому пристрої.
- ВИ
- ПРИСТРІЙ
+ Ви
+ ПристрійВимкнути
- ТЕМИ
- ПОВІДОМЛЕННЯ ТА ФАЙЛИ
+ Теми
+ Повідомлення та файлиЧат працюєІмпортувати базу данихСтарий архів бази даних
@@ -782,7 +782,7 @@
місяціВи вже підключені до %1$s через це посилання.Режим інкогніто
- СЕРВЕРИ
+ СервериЗберегти вітальне повідомлення?Отримання черезПриглушено, коли неактивно!
@@ -879,7 +879,7 @@
%d день%d днівскасовано %s
- ЗАПУСК ЧАТУ
+ Запуск чатуПароль бази данихЕкспортувати базу данихВидалити всі файли
@@ -930,7 +930,7 @@
змінює адресу…Залишитиспостерігач
- УЧАСНИК
+ УчасникРежим інкогніто захищає вашу конфіденційність, використовуючи новий випадковий профіль для кожного контакту.Більше поліпшень незабаром!Тільки власники груп можуть увімкнути голосові повідомлення.
@@ -979,7 +979,7 @@
Контакт відміченоВи намагаєтеся запросити контакт, з яким ви поділилися інкогніто-профілем, до групи, в якій ви використовуєте основний профільПомилка при створенні посилання на групу
- ДЛЯ КОНСОЛІ
+ Для консоліУчасника буде вилучено з групи - цю дію неможливо скасувати!Змінити рольВи все ще отримуватимете дзвінки та сповіщення від приглушених профілів, коли вони активні.
@@ -1021,7 +1021,7 @@
ХостПортОбов\'язково
- КОЛЬОРИ ІНТЕРФЕЙСУ
+ Кольори інтерфейсуСтворіть адресу, щоб дозволити людям підключатися до вас.Контакти залишатимуться підключеними.Створити SimpleX-адресу
@@ -1110,7 +1110,7 @@
ГалереяКоманда SimpleXхоче підключитися до вас!
- ЕКСПЕРИМЕНТАЛЬНІ ФУНКЦІЇ
+ Експериментальні функціїВи повинні використовувати найновішу версію бази даних чату лише на одному пристрої, інакше ви можете припинити отримання повідомлень від деяких контактів.Цей параметр застосовується до повідомлень у вашому поточному профілі чатуЗашифрована база даних
@@ -1163,7 +1163,7 @@
Кожен може хостити сервери.Інструменти розробникаЕкспериментальні функції
- ДЗВІНКИ
+ ДзвінкиЗберегти ключову фразу в сховищі ключівПомилка шифрування бази данихВилучити ключову фразу із сховища ключів?
@@ -1259,7 +1259,7 @@
Заборонено файли та медіа!Буде відправлено ваш профіль %1$s.Вимкнути для всіх груп
- НАДСИЛАТИ ПОВІДОМЛЕННЯ ПРО ДОСТАВКУ
+ Надсилати повідомлення про доставкуПідключитися безпосередньо?%s: %sЗапит на підключення буде відправлено учаснику групи.
@@ -1734,7 +1734,7 @@
Звуки вхідного дзвінкаСвітлий режимЗапасний варіант маршрутизації повідомлень
- МАРШРУТИЗАЦІЯ ПРИВАТНИХ ПОВІДОМЛЕНЬ
+ Маршрутизація приватних повідомленьпересланоІншеДозволити надсилати посилання SimpleX.
@@ -1746,7 +1746,7 @@
Камера та мікрофонНадайте дозвіл(и) на здійснення дзвінківВідкрити налаштування
- ФАЙЛИ
+ ФайлиЗображення профілівПідключення до мережіадміністратори
@@ -2049,7 +2049,7 @@
Скинути всі підказкиДоступно оновлення: %sЗавантаження оновлення скасовано
- БАЗА ДАНИХ ЧАТУ
+ База даних чатуВибрати профіль чатуПомилка при зміні профілюПовідомлення будуть видалені — це не можна скасувати!
@@ -2513,7 +2513,7 @@
Дозвольте своїм контактам надсилати файли та медіа.БотВи, і ваш контакт можете надсилати файли та медіа.
- ЗАПИТИ НА ЗВ’ЯЗОК ВІД ГРУП
+ Запити на зв’язок від групЗастарілі опціїПомилка при відмітці як прочитанеФайли та медіа заборонені у цьому чаті.
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml
index 235158585d..93347fa228 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml
@@ -93,14 +93,14 @@
Ứng dụng chỉ có thể nhận thông báo khi nó đang chạy, không có dịch vụ nền nào được khởi độngBản dựng ứng dụng: %sGiao diện
- ỨNG DỤNG
+ Ứng dụngDi chuyển dữ liệu ứng dụngSao lưu dữ liệu ứng dụngMã truy cập ứng dụng đã được thay thế bằng mã tự hủy.Ứng dụng mã hóa các tệp cục bộ mới (trừ video).Áp dụngMã truy cập ứng dụng
- BIỂU TƯỢNG ỨNG DỤNG
+ Biểu tượng ứng dụngMã truy cậpPhiên bản ứng dụng: v%sPhiên bản ứng dụng
@@ -184,7 +184,7 @@
Cuộc gọi kết thúccuộc gọi kết thúc %1$slỗi cuộc gọi
- CUỘC GỌI
+ Cuộc gọiHủy xem trước ảnhHủy xem trước tệpHủy
@@ -234,11 +234,11 @@
đang thay đổi địa chỉ cho %s…Tùy chọn trò chuyệnMàu trò chuyện
- CƠ SỞ DỮ LIỆU TRÒ CHUYỆN
+ Cơ sở dữ liệu trò chuyệnKết nối trò chuyện đã được dừng lạiCơ sở dữ liệu đã được di chuyển!Các cuộc trò chuyện
- CÁC CUỘC TRÒ CHUYỆN
+ Các cuộc trò chuyệnKiểm tra tin nhắn mới mỗi 10 phút trong tối đa 1 phútGiao diện Trung Quốc và Tây Ban NhaTrò chuyện với nhà phát triển
@@ -463,7 +463,7 @@
Xóa máy chủXóa hàng đợiCông cụ nhà phát triển
- THIẾT BỊ
+ Thiết bịTùy chọn cho nhà phát triểnXác thực thiết bị đã bị vô hiệu hóa. Tắt Khóa SimpleX.Lỗi máy chủ đích: %1$s
@@ -738,7 +738,7 @@
Lỗi cập nhật cấu hình mạngLỗi cập nhật quyền riêng tư người dùngMở rộng chọn chức vụ
- THỬ NGHIỆM
+ Thử nghiệmMở rộngThoát mà không lưuđã hết hạn
@@ -748,7 +748,7 @@
Xuất cơ sở dữ liệuLỗi tải lên kho lưu trữTập tin đã xuất không tồn tại
- TẬP TIN
+ Tập tinKhông thể tải các cuộc trò chuyệnKhông tìm thấy tệp - có thể tập tin đã bị xóa và hủy bỏ.Lỗi tệp
@@ -776,7 +776,7 @@
Tệp sẽ được nhận khi liên hệ của bạn hoạt động, vui lòng chờ hoặc kiểm tra lại sau!Trạng thái tệp: %sLấp đầy
- CƠ SỞ DỮ LIỆU TRÒ CHUYỆN
+ Cơ sở dữ liệu trò chuyệnLỗi chuyển đổi hồ sơLọc các cuộc hội thoại chưa đọc và các cuộc hội thoại yêu thích.Cuối cùng, chúng ta đã có chúng! 🚀
@@ -821,7 +821,7 @@
Máy chủ chuyển tiếp %1$s không thể kết nối tới máy chủ đích %2$s. Vui lòng thử lại sau.Địa chỉ máy chủ chuyển tiếp không tương thích với cài đặt mạng: %1$s.Phiên bản máy chủ chuyển tiếp không tương thích với cài đặt mạng: %1$s.
- CHO CONSOLE
+ Cho consoleChuyển tiếp tin nhắn…Giảm thiểu sử dụng pin hơn nữaChuyển tiếp tin nhắn mà không có tệp?
@@ -863,7 +863,7 @@
Xin chào!
\nKết nối với tôi qua SimpleX Chat: %sẨn hồ sơ
- TRỢ GIÚP
+ Trợ giúpNhóm sẽ bị xóa cho tất cả các thành viên - điều này không thể hoàn tác!Nhóm sẽ bị xóa cho bạn - điều này không thể hoàn tác!Tùy chọn nhóm
@@ -952,7 +952,7 @@
Cài đặt cập nhậtCuộc gọi video đếnPhiên bản không tương thích
- MÀU SẮC GIAO DIỆN
+ Màu sắc giao diệnđã được mờiĐường dẫn không hợp lệcuộc trò chuyện không hợp lệ
@@ -1001,7 +1001,7 @@
Mời thành viênMời vào nhómRời nhóm
- THÀNH VIÊN
+ Thành viênThông tin hàng đợi tin nhắnChỉ dữ liệu hồ sơ cục bộGiữ lại các kết nối của bạn
@@ -1050,7 +1050,7 @@
Thành viên sẽ bị xóa khỏi nhóm - việc này không thể được hoàn tác!Chế độ sángUI tiếng Litva
- TIN NHẮN VÀ TỆP
+ Tin nhắn và tệpViệc xóa tin nhắn mà không thể phục hồi là bị cấm.Tham gia vào các cuộc trò chuyện nhómChế độ định tuyến tin nhắn
@@ -1323,7 +1323,7 @@
Mật khẩu hồ sơGhi chú riêng tưCấm xóa tin nhắn mà không thể phục hồi.
- ĐỊNH TUYẾN TIN NHẮN RIÊNG TƯ
+ Định tuyến tin nhắn riêng tưTên hồ sơ:ảnh đại diệnHồ sơ và các kết nối máy chủ
@@ -1581,7 +1581,7 @@
Đặt lại tất cả số liệu thống kê?Lưu mật khẩu và mở kết nối trò chuyệnGửi
- KHỞI CHẠY KẾT NỐI TRÒ CHUYỆN
+ Khởi chạy kết nối trò chuyệnLưuQuét / Dán đường dẫnQuét mã QR máy chủ
@@ -1623,7 +1623,7 @@
Lưu lời chào?gửi thất bạiQuét mã bảo mật từ ứng dụng của liên hệ bạn.
- GỬI CHỈ BÁO ĐÃ NHẬN TỚI
+ Gửi chỉ báo đã nhận tớiTìm kiếmTìm kiếm hoặc dán đường dẫn SimpleXLưu danh sách
@@ -1685,7 +1685,7 @@
đặt ảnh đại diện mớiCác tin nhắn đã gửi sẽ bị xóa sau thời gian đã cài.thông tin hàng đợi máy chủ: %1$s\n\ntin nhắn được nhận cuối cùng: %2$s
- CÀI ĐẶT
+ Cài đặtĐã gửi vàoĐịa chỉ máy chủMã phiên
@@ -1716,7 +1716,7 @@
Đặt mật khẩuCài đặtĐịa chỉ máy chủ không tương thích với cài đặt mạng: %1$s.
- CÁC MÁY CHỦ
+ Các máy chủMáy chủ yêu cầu xác thực để tải lên, kiểm tra mật khẩuMáy chủĐặt mã truy cập
@@ -1828,7 +1828,7 @@
Loa ngoài bậtÂm thanh đã bị tắtỔn định
- PROXY SOCKS
+ Proxy SOCKSCác nhóm nhỏ (tối đa 20 thành viên)Một vài lỗi không nghiêm trọng đã xảy ra trong lúc nhập:Loa ngoài tắt
@@ -1896,7 +1896,7 @@
Xin gửi lời cảm ơn tới các người dùng đã góp công qua Weblate!Xin gửi lời cảm ơn tới các người dùng đã góp công qua Weblate!Chế độ hệ thống
- HỖ TRỢ SIMPLEX CHAT
+ Hỗ trợ SimpleX ChatLỗi tệp tạm thờiNhấn nútKết nối TCP
@@ -1947,7 +1947,7 @@
ID của tin nhắn tiếp theo là không chính xác (nhỏ hơn hoặc bằng với cái trước).\nViệc này có thể xảy ra do một vài lỗi hoặc khi kết nối bị xâm phạm.Nỗ lực đổi mật khẩu cơ sở dữ liệu đã không được hoàn thành.Tên thiết bị sẽ được chia sẻ với thiết bị di động đã được kết nối.
- CÁC CHỦ ĐỀ
+ Các chủ đềTên hiển thị này không hợp lệ. Xin vui lòng chọn một cái tên khác.Hồ sơ chỉ được chia sẻ với các liên hệ của bạn.Cuộc trò chuyện này được bảo vệ bằng mã hóa đầu cuối có kháng lượng tử.
@@ -2196,7 +2196,7 @@
Bạn có thể thay đổi nói trong cài đặt Giao diện.Bạn đang tham gia nhóm thông qua đường dẫn này.Bạn có thể bật vào lúc sau thông qua Cài đặt
- BẠN
+ BạnBạn có thể chia sẻ một đường dẫn hoặc mã QR - bất kỳ ai cũng sẽ có thể tham gia nhóm. Bạn sẽ không mất các thành viên của nhóm nếu sau này bạn xóa nó đi.Bạn có thể thử một lần nữa.Bạn đã kết nối tới máy chủ dùng để nhận tin nhắn từ liên hệ này.
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml
index 1392d7b42b..3b767b97b4 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml
@@ -754,7 +754,7 @@
下一代私密通讯软件粘贴你收到的链接已跳过消息
- 支持 SIMPLEX CHAT
+ 支持 SimpleX Chat发送链接预览SOCKS 代理停止聊天程序?
@@ -2670,7 +2670,7 @@
%1$d 个中继失灵%1$d 个中继不活跃删除了 %1$d 个中继
- 目前不支持添加中继。
+ 添加恢复消息传输的中继。所有中继均失灵删除了所有中继无法广播
@@ -2785,4 +2785,38 @@
底部栏将通过 SOCKS5 代理请求链接预览。DNS 查询仍可能通过你的 DNS 解析器在本地发生。顶部栏
+ 你的新频道 %1$s 已连接到 %3$d 个中继中的 %2$d 个中继。\n如果取消,频道将被删除 —— 你可以再次创建它。
+ 这是上次活跃的中继。删除它会阻止给订阅者传送消息。
+ 添加
+ 添加中继
+ 添加中继
+ 取消并删除频道
+ 选中了 %d 个中继
+ 添加中继出错
+ 无可用中继
+ 无中继
+ 未选中中继
+ 已添加的中继:%1$s。
+ 将从频道删除中继 — 这无法撤销!
+ 已删除
+ 删除中继
+ 删除中继?
+ 选择中继
+ 如果选择关闭将不会接收消息。\n可以之后在外观设置中更改。
+ 保持 SimpleX 在后台运行以接收消息。
+ 最小化到托盘
+ 最小化到托盘?
+ 关闭窗口时最小化到托盘
+ 退出 SimpleX
+ 显示 SimpleX
+ SimpleX
+ SimpleX — %d 则未读
+ 关闭应用
+ 删除消息出错
+ 来自历史记录
+ 另一个应用实例可能正在运行或没有正确退出。仍要启动?
+ 应用正在运行
+ 被拒绝
+ 被中继运营方拒绝
+ 状态
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml
index 9ec116058a..9e51b9ba9d 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml
@@ -668,7 +668,7 @@
裝置幫助設定
- 幫助 SIMPLEX CHAT
+ 幫助 SimpleX Chat聊天開發者工具SOCKS 代理伺服器
diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/ui.js b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/ui.js
index 7c0836960c..e6828817ee 100644
--- a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/ui.js
+++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/ui.js
@@ -3,7 +3,7 @@
useWorker = typeof window.Worker !== "undefined";
isDesktop = true;
// Create WebSocket connection.
-const socket = new WebSocket(`ws://${location.host}`);
+const socket = new WebSocket(`ws://${location.host}${location.search}`);
socket.addEventListener("open", (_event) => {
console.log("Opened socket");
sendMessageToNative = (msg) => {
@@ -192,4 +192,4 @@ function updateCallInfoView(state, description) {
document.getElementById("state").innerText = state;
document.getElementById("description").innerText = description;
}
-//# sourceMappingURL=ui.js.map
\ No newline at end of file
+//# sourceMappingURL=ui.js.map
diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt
index 97de08b07e..7ea41d3593 100644
--- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt
+++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt
@@ -10,7 +10,7 @@ val desktopPlatform = detectDesktopPlatform()
enum class DesktopPlatform(val libExtension: String, val configPath: String, val dataPath: String, val githubAssetName: String) {
LINUX_X86_64("so", unixConfigPath, unixDataPath, "simplex-desktop-x86_64.AppImage"),
- LINUX_AARCH64("so", unixConfigPath, unixDataPath, " simplex-desktop-aarch64.AppImage"),
+ LINUX_AARCH64("so", unixConfigPath, unixDataPath, "simplex-desktop-aarch64.AppImage"),
WINDOWS_X86_64("dll", System.getenv("AppData") + File.separator + "SimpleX", System.getenv("AppData") + File.separator + "SimpleX", "simplex-desktop-windows-x86_64.msi"),
MAC_X86_64("dylib", unixConfigPath, unixDataPath, "simplex-desktop-macos-x86_64.dmg"),
MAC_AARCH64("dylib", unixConfigPath, unixDataPath, "simplex-desktop-macos-aarch64.dmg");
diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt
index 90c80d3b2a..c3b6dc3a4c 100644
--- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt
+++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt
@@ -6,6 +6,7 @@ import androidx.compose.ui.graphics.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.*
+import org.jetbrains.compose.videoplayer.SkiaBitmapVideoSurface
import uk.co.caprica.vlcj.media.VideoOrientation
import uk.co.caprica.vlcj.player.base.*
import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent
@@ -214,7 +215,7 @@ actual class VideoPlayer actual constructor(
}
}
- suspend fun getBitmapFromVideo(defaultPreview: ImageBitmap?, uri: URI?, withAlertOnException: Boolean = true): VideoPlayerInterface.PreviewAndDuration = withContext(playerThread.asCoroutineDispatcher()) {
+ suspend fun getBitmapFromVideo(defaultPreview: ImageBitmap?, uri: URI?, withAlertOnException: Boolean = true): VideoPlayerInterface.PreviewAndDuration = withContext(previewThread.asCoroutineDispatcher()) {
val mediaComponent = getOrCreateHelperPlayer()
val player = mediaComponent.mediaPlayer()
if (uri == null || !uri.toFile().exists()) {
@@ -222,12 +223,12 @@ actual class VideoPlayer actual constructor(
return@withContext VideoPlayerInterface.PreviewAndDuration(preview = defaultPreview, timestamp = 0L, duration = 0L)
}
+ val surface = SkiaBitmapVideoSurface()
+ player.videoSurface().set(surface)
player.media().startPaused(uri.toFile().absolutePath)
- val start = System.currentTimeMillis()
- var snap: BufferedImage? = null
- while (snap == null && start + 1500 > System.currentTimeMillis()) {
- snap = player.snapshots()?.get()
- delay(50)
+ val snap = withTimeoutOrNull(1500L) {
+ while (surface.bitmap.value == null) delay(50)
+ surface.bitmap.value!!.toAwtImage()
}
val orientation = player.media().info().videoTracks().firstOrNull()?.orientation()
if (orientation == null) {
@@ -255,6 +256,7 @@ actual class VideoPlayer actual constructor(
}
val playerThread = Executors.newSingleThreadExecutor()
+ private val previewThread = Executors.newSingleThreadExecutor()
private val playersPool: ArrayList = ArrayList()
private val helperPlayersPool: ArrayList = ArrayList()
diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt
index 20fe6a48a3..75782d75d7 100644
--- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt
+++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt
@@ -18,10 +18,12 @@ import org.nanohttpd.protocols.http.response.Status
import org.nanohttpd.protocols.websockets.*
import java.io.IOException
import java.net.BindException
-import java.net.URI
+import java.security.SecureRandom
+import java.util.Base64
private const val SERVER_HOST = "localhost"
private const val SERVER_PORT = 50395
+private const val CALL_SERVER_TOKEN_BYTES = 32
val connections = ArrayList()
// Spec: spec/services/calls.md#ActiveCallView
@@ -153,14 +155,15 @@ private fun SendStateUpdates() {
@Composable
fun WebRTCController(callCommand: SnapshotStateList, onResponse: (WVAPIMessage) -> Unit) {
val uriHandler = LocalUriHandler.current
+ val token = remember { newCallServerToken() }
val endCall = {
val call = chatModel.activeCall.value
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
}
val server = remember {
- startServer(onResponse).apply {
+ startServer(onResponse, token = token).apply {
try {
- uriHandler.openUri("http://${SERVER_HOST}:${listeningPort}/simplex/call/")
+ uriHandler.openUri("http://${SERVER_HOST}:${listeningPort}/simplex/call/?token=$token")
} catch (e: Exception) {
Log.e(TAG, "Unable to open browser: ${e.stackTraceToString()}")
AlertManager.shared.showAlertMsg(
@@ -208,7 +211,11 @@ fun WebRTCController(callCommand: SnapshotStateList, onResponse: (
}
}
-fun startServer(onResponse: (WVAPIMessage) -> Unit, port: Int = SERVER_PORT): NanoWSD {
+fun startServer(
+ onResponse: (WVAPIMessage) -> Unit,
+ port: Int = SERVER_PORT,
+ token: String = newCallServerToken(),
+): NanoWSD {
val server = object: NanoWSD(SERVER_HOST, port) {
override fun openWebSocket(session: IHTTPSession): WebSocket = MyWebSocket(onResponse, session)
@@ -227,8 +234,18 @@ fun startServer(onResponse: (WVAPIMessage) -> Unit, port: Int = SERVER_PORT): Na
override fun handle(session: IHTTPSession): Response {
return when {
- session.headers["upgrade"] == "websocket" -> super.handle(session)
- session.uri.contains("/simplex/call/") -> resourcesToResponse("/desktop/call.html")
+ session.headers["upgrade"] == "websocket" ->
+ if (hasValidCallServerToken(session.parameters, token)) {
+ super.handle(session)
+ } else {
+ unauthorizedResponse()
+ }
+ session.uri.contains("/simplex/call/") ->
+ if (hasValidCallServerToken(session.parameters, token)) {
+ resourcesToResponse("/desktop/call.html")
+ } else {
+ unauthorizedResponse()
+ }
else -> resourcesToResponse(uriCreateOrNull(session.uri)?.path ?: return newFixedLengthResponse("Error parsing URL"))
}
}
@@ -239,11 +256,23 @@ fun startServer(onResponse: (WVAPIMessage) -> Unit, port: Int = SERVER_PORT): Na
if (port == 0) throw e
Log.w(TAG, "Call server port $port is busy, using a random port: ${e.message}")
server.stop()
- return startServer(onResponse, port = 0)
+ return startServer(onResponse, port = 0, token = token)
}
return server
}
+internal fun newCallServerToken(): String {
+ val bytes = ByteArray(CALL_SERVER_TOKEN_BYTES)
+ SecureRandom().nextBytes(bytes)
+ return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes)
+}
+
+internal fun hasValidCallServerToken(parameters: Map>, token: String): Boolean =
+ token.isNotEmpty() && parameters["token"]?.any { it == token } == true
+
+private fun unauthorizedResponse(): Response =
+ newFixedLengthResponse(Status.UNAUTHORIZED, "text/plain", "Unauthorized")
+
class MyWebSocket(val onResponse: (WVAPIMessage) -> Unit, handshakeRequest: IHTTPSession) : WebSocket(handshakeRequest) {
override fun onOpen() {
connections.add(this)
diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt
index a1f70213d0..c875c0c9da 100644
--- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt
+++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt
@@ -1,6 +1,5 @@
package chat.simplex.common.views.chatlist
-import SectionDivider
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.layout.*
@@ -62,6 +61,6 @@ actual fun ChatListNavLinkLayout(
if (selectedChat.value || nextChatSelected.value) {
Divider()
} else {
- SectionDivider()
+ Divider(Modifier.padding(horizontal = 8.dp))
}
}
diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt
index 52e845b422..43428bab72 100644
--- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt
+++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt
@@ -12,7 +12,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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
@@ -67,15 +66,17 @@ actual fun UserPickerUsersSection(
}
}
- Text(
- user.displayName,
- fontSize = 12.sp,
- fontWeight = if (user.activeUser) FontWeight.Bold else FontWeight.Normal,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- modifier = Modifier.width(65.dp),
- textAlign = TextAlign.Center
- )
+ Row(Modifier.width(65.dp), horizontalArrangement = Arrangement.Center) {
+ Text(
+ user.displayName,
+ fontSize = 12.sp,
+ fontWeight = if (user.activeUser) FontWeight.Bold else FontWeight.Normal,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.alignByBaseline().weight(1f, fill = false)
+ )
+ NameBadge(user.profile.localBadge, 12.sp)
+ }
}
}
}
diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt
index 974578882d..f6a6023d47 100644
--- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt
+++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt
@@ -26,6 +26,8 @@ import java.io.Closeable
import java.io.File
import java.net.InetSocketAddress
import java.net.Proxy
+import java.nio.file.Files
+import java.nio.file.StandardCopyOption
import kotlin.math.min
data class SemVer(
@@ -376,7 +378,7 @@ private fun chooseGitHubReleaseAssets(release: GitHubRelease): List
val res = if (isRunningFromFlatpak()) {
// No need to show download options for Flatpak users
emptyList()
- } else if (!isRunningFromAppImage() && Runtime.getRuntime().exec("which dpkg").onExit().join().exitValue() == 0) {
+ } else if (desktopPlatform.isLinux() && !isRunningFromAppImage() && Runtime.getRuntime().exec("which dpkg").onExit().join().exitValue() == 0) {
// Show all available .deb packages and user will choose the one that works on his system (for Debian derivatives)
release.assets.filter { it.name.lowercase().endsWith(".deb") }
} else {
@@ -388,18 +390,42 @@ private fun chooseGitHubReleaseAssets(release: GitHubRelease): List
private suspend fun installAppUpdate(file: File) = withContext(Dispatchers.IO) {
when {
desktopPlatform.isLinux() -> {
- val process = Runtime.getRuntime().exec("xdg-open ${file.absolutePath}").onExit().join()
- val startedInstallation = process.exitValue() == 0 && process.children().count() > 0
- if (!startedInstallation) {
- Log.e(TAG, "Error starting installation: ${process.inputReader().use { it.readLines().joinToString("\n") }}${process.errorStream.use { String(it.readAllBytes()) }}")
- // Failed to start installation. show directory with the file for manual installation
- desktopOpenDir(file.parentFile)
+ val appImagePath = System.getenv("APPIMAGE")
+ if (appImagePath != null) {
+ // Replace the running AppImage crash-safely: copy onto the target's own
+ // filesystem first (an atomic rename only works within one filesystem, and
+ // the download lives in the temp dir which is usually a different one),
+ // then atomically move the staged file onto $APPIMAGE.
+ val target = File(appImagePath)
+ val staging = File(target.parentFile, ".${target.name}.update")
+ try {
+ Files.copy(file.toPath(), staging.toPath(), StandardCopyOption.REPLACE_EXISTING)
+ staging.setExecutable(true, false)
+ Files.move(staging.toPath(), target.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING)
+ file.delete()
+ AlertManager.shared.showAlertMsg(
+ title = generalGetString(MR.strings.app_check_for_updates_installed_successfully_title),
+ text = generalGetString(MR.strings.app_check_for_updates_installed_successfully_desc)
+ )
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to replace AppImage: ${e.stackTraceToString()}")
+ staging.delete()
+ desktopOpenDir(file.parentFile)
+ }
} else {
- AlertManager.shared.showAlertMsg(
- title = generalGetString(MR.strings.app_check_for_updates_installed_successfully_title),
- text = generalGetString(MR.strings.app_check_for_updates_installed_successfully_desc)
- )
- file.delete()
+ val process = Runtime.getRuntime().exec("xdg-open ${file.absolutePath}").onExit().join()
+ val startedInstallation = process.exitValue() == 0 && process.children().count() > 0
+ if (!startedInstallation) {
+ Log.e(TAG, "Error starting installation: ${process.inputReader().use { it.readLines().joinToString("\n") }}${process.errorStream.use { String(it.readAllBytes()) }}")
+ // Failed to start installation. show directory with the file for manual installation
+ desktopOpenDir(file.parentFile)
+ } else {
+ AlertManager.shared.showAlertMsg(
+ title = generalGetString(MR.strings.app_check_for_updates_installed_successfully_title),
+ text = generalGetString(MR.strings.app_check_for_updates_installed_successfully_desc)
+ )
+ file.delete()
+ }
}
}
desktopPlatform.isWindows() -> {
diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt
index 66be736fca..6b36a3b1b2 100644
--- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt
+++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt
@@ -1,10 +1,11 @@
package chat.simplex.common.views.usersettings
+import CARD_PADDING
import SectionBottomSpacer
import SectionDividerSpaced
-import SectionSpacer
import SectionTextFooter
import SectionView
+import itemHPadding
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -23,7 +24,7 @@ import chat.simplex.common.model.CloseBehavior
import chat.simplex.common.model.SharedPreference
import chat.simplex.common.trayIsAvailable
import chat.simplex.common.platform.*
-import chat.simplex.common.ui.theme.DEFAULT_PADDING
+import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
@@ -82,10 +83,10 @@ fun AppearanceScope.AppearanceLayout(
SectionDividerSpaced()
ProfileImageSection()
- SectionDividerSpaced(maxTopPadding = true)
+ SectionDividerSpaced()
FontScaleSection()
- SectionDividerSpaced(maxTopPadding = true)
+ SectionDividerSpaced()
DensityScaleSection()
SectionBottomSpacer()
@@ -110,8 +111,8 @@ private fun MinimizeToTraySection() {
@Composable
fun DensityScaleSection() {
val localDensityScale = remember { mutableStateOf(appPrefs.densityScale.get()) }
- SectionView(stringResource(MR.strings.appearance_zoom).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
- Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) {
+ SectionView(stringResource(MR.strings.appearance_zoom), contentPadding = PaddingValues(horizontal = CARD_PADDING)) {
+ Row(Modifier.padding(vertical = 10.dp), verticalAlignment = Alignment.CenterVertically) {
Box(Modifier.size(50.dp)
.background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22))
.clip(RoundedCornerShape(percent = 22))
diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt
index 5b4a044df3..174ad63c7a 100644
--- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt
+++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt
@@ -1,27 +1,21 @@
package chat.simplex.common.views.usersettings
import SectionView
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.padding
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
+import androidx.compose.runtime.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel
-import chat.simplex.common.platform.AppUpdatesChannel
-import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
+import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@Composable
-actual fun SettingsSectionApp(
+actual fun AdvancedSettingsAppSection(
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
- showVersion: () -> Unit,
- withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
+ withAuth: (title: String, desc: String, block: () -> Unit) -> Unit,
) {
- SectionView(stringResource(MR.strings.settings_section_title_app)) {
+ SectionView {
SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(withAuth) })
val selectedChannel = remember { appPrefs.appUpdateChannel.state }
val values = AppUpdatesChannel.entries.map { it to it.text }
@@ -29,6 +23,8 @@ actual fun SettingsSectionApp(
appPrefs.appUpdateChannel.set(it)
setupUpdateChecker()
}
- AppVersionItem(showVersion)
}
}
+
+@Composable
+actual fun AppShutdownItem() {}
diff --git a/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/CallServerAuthTest.kt b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/CallServerAuthTest.kt
new file mode 100644
index 0000000000..800c69f617
--- /dev/null
+++ b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/CallServerAuthTest.kt
@@ -0,0 +1,70 @@
+package chat.simplex.app
+
+import chat.simplex.common.views.call.startServer
+import java.net.Socket
+import kotlin.test.AfterTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+
+// Integration test for the desktop call server's token gate (the handle() enforcement),
+// which the unit-level CallServerTokenTest does not exercise.
+class CallServerAuthTest {
+ private val token = "integration-test-token"
+ // port = 0 binds a random free port, avoiding a clash with a real call server on SERVER_PORT
+ private val server = startServer(onResponse = {}, port = 0, token = token)
+ private val port get() = server.listeningPort
+
+ @AfterTest
+ fun tearDown() = server.stop()
+
+ @Test
+ fun testWebSocketUpgradeRejectedWithoutToken() {
+ assertEquals(401, requestStatus(webSocketUpgrade(path = "/")))
+ }
+
+ @Test
+ fun testWebSocketUpgradeRejectedWithWrongToken() {
+ assertEquals(401, requestStatus(webSocketUpgrade(path = "/?token=wrong")))
+ }
+
+ @Test
+ fun testWebSocketUpgradeAcceptedWithToken() {
+ assertEquals(101, requestStatus(webSocketUpgrade(path = "/?token=$token")))
+ }
+
+ @Test
+ fun testCallPageRejectedWithoutToken() {
+ assertEquals(401, requestStatus(get(path = "/simplex/call/")))
+ }
+
+ @Test
+ fun testCallPagePassesAuthGateWithToken() {
+ // Resource serving may differ in the test classpath, so assert only that the auth gate was passed (not 401)
+ assertNotEquals(401, requestStatus(get(path = "/simplex/call/?token=$token")))
+ }
+
+ private fun get(path: String): List = listOf("GET $path HTTP/1.1", "Host: localhost:$port")
+
+ private fun webSocketUpgrade(path: String): List =
+ listOf(
+ "GET $path HTTP/1.1",
+ "Host: localhost:$port",
+ "Upgrade: websocket",
+ "Connection: Upgrade",
+ "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==",
+ "Sec-WebSocket-Version: 13",
+ )
+
+ // Sends a raw HTTP request and returns the response status code from the status line.
+ private fun requestStatus(requestLines: List): Int =
+ Socket("localhost", port).use { socket ->
+ socket.soTimeout = 5000
+ socket.getOutputStream().apply {
+ write((requestLines.joinToString("\r\n") + "\r\n\r\n").toByteArray())
+ flush()
+ }
+ val statusLine = socket.getInputStream().bufferedReader().readLine() ?: error("no response from call server")
+ statusLine.split(" ")[1].toInt()
+ }
+}
diff --git a/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/CallServerTokenTest.kt b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/CallServerTokenTest.kt
new file mode 100644
index 0000000000..dc729b1ec2
--- /dev/null
+++ b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/CallServerTokenTest.kt
@@ -0,0 +1,29 @@
+package chat.simplex.app
+
+import chat.simplex.common.views.call.hasValidCallServerToken
+import chat.simplex.common.views.call.newCallServerToken
+import kotlin.test.Test
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class CallServerTokenTest {
+ @Test
+ fun testCallServerTokenRequiresExactTokenParameter() {
+ val token = "secret"
+
+ assertTrue(hasValidCallServerToken(mapOf("token" to listOf(token)), token))
+ assertFalse(hasValidCallServerToken(mapOf("token" to listOf("wrong")), token))
+ assertFalse(hasValidCallServerToken(mapOf("x-token" to listOf(token)), token))
+ assertFalse(hasValidCallServerToken(mapOf("token" to listOf(token)), ""))
+ }
+
+ @Test
+ fun testCallServerTokenIsUrlSafe() {
+ val token = newCallServerToken()
+
+ assertTrue(token.length >= 40)
+ assertFalse(token.contains("+"))
+ assertFalse(token.contains("/"))
+ assertFalse(token.contains("="))
+ }
+}
diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties
index 4d504e069e..61c744bf86 100644
--- a/apps/multiplatform/gradle.properties
+++ b/apps/multiplatform/gradle.properties
@@ -24,13 +24,13 @@ android.nonTransitiveRClass=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11
-android.version_name=6.5.2
-android.version_code=349
+android.version_name=6.5.5
+android.version_code=355
android.bundle=false
-desktop.version_name=6.5.2
-desktop.version_code=143
+desktop.version_name=6.5.5
+desktop.version_code=146
kotlin.version=2.1.20
gradle.plugin.version=8.7.0
diff --git a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs
index ff853f403d..268e4329cc 100644
--- a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs
+++ b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs
@@ -94,5 +94,5 @@ mkChatOpts BroadcastBotOpts {coreOptions, botDisplayName} =
autoAcceptFileSize = 0,
muteNotifications = True,
markRead = False,
- createBot = Just CreateBotOpts {botDisplayName, allowFiles = False}
+ createBot = Just CreateBotOpts {botDisplayName, allowFiles = False, clientService = False}
}
diff --git a/apps/simplex-chat/Main.hs b/apps/simplex-chat/Main.hs
index 41321edc68..f0501fef4f 100644
--- a/apps/simplex-chat/Main.hs
+++ b/apps/simplex-chat/Main.hs
@@ -1,8 +1,14 @@
module Main where
import Server (simplexChatServer)
+import Simplex.Chat.Badges.CLI (runBadgeCommand)
import Simplex.Chat.Terminal (terminalChatConfig)
import Simplex.Chat.Terminal.Main (simplexChatCLI)
+import System.Environment (getArgs)
main :: IO ()
-main = simplexChatCLI terminalChatConfig (Just simplexChatServer)
+main = do
+ args <- getArgs
+ case args of
+ ("badge" : _) -> runBadgeCommand args
+ _ -> simplexChatCLI terminalChatConfig (Just simplexChatServer)
diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs
index f566ed5ded..5d51023781 100644
--- a/apps/simplex-directory-service/src/Directory/Options.hs
+++ b/apps/simplex-directory-service/src/Directory/Options.hs
@@ -39,6 +39,7 @@ data DirectoryOpts = DirectoryOpts
directoryLog :: Maybe FilePath,
migrateDirectoryLog :: Maybe MigrateLog,
serviceName :: T.Text,
+ clientService :: Bool,
runCLI :: Bool,
searchResults :: Int,
webFolder :: Maybe FilePath,
@@ -151,6 +152,11 @@ directoryOpts appDir defaultDbName = do
<> help "The display name of the directory service bot, without *'s and spaces (SimpleX Directory)"
<> value "SimpleX Directory"
)
+ clientService <-
+ switch
+ ( long "client-service"
+ <> help "Use client service certificate"
+ )
runCLI <-
switch
( long "run-cli"
@@ -188,6 +194,7 @@ directoryOpts appDir defaultDbName = do
directoryLog,
migrateDirectoryLog,
serviceName = T.pack serviceName,
+ clientService,
runCLI,
searchResults = 10,
webFolder,
@@ -207,7 +214,7 @@ getDirectoryOpts appDir defaultDbName =
versionAndUpdate = versionStr <> "\n" <> updateStr
mkChatOpts :: DirectoryOpts -> ChatOpts
-mkChatOpts DirectoryOpts {coreOptions, serviceName} =
+mkChatOpts DirectoryOpts {coreOptions, serviceName, clientService} =
ChatOpts
{ coreOptions,
chatCmd = "",
@@ -221,7 +228,7 @@ mkChatOpts DirectoryOpts {coreOptions, serviceName} =
autoAcceptFileSize = 0,
muteNotifications = True,
markRead = False,
- createBot = Just CreateBotOpts {botDisplayName = serviceName, allowFiles = False}
+ createBot = Just CreateBotOpts {botDisplayName = serviceName, allowFiles = False, clientService}
}
parseMigrateLog :: ReadM MigrateLog
diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs
index 6e414ef011..63e1a0ff69 100644
--- a/apps/simplex-directory-service/src/Directory/Service.hs
+++ b/apps/simplex-directory-service/src/Directory/Service.hs
@@ -204,7 +204,7 @@ linkCheckThread_ opts env@ServiceState {eventQ}
threadDelay $ linkCheckInterval opts * 1000000
u <- readTVarIO $ currentUser cc
forM_ u $ \user ->
- withDB' "linkCheckThread" cc (\db -> getAllGroupRegs_ db user) >>= \case
+ withDB' "linkCheckThread" cc (\db -> getAllGroupRegs_ db (storeCxt cc) user) >>= \case
Left e -> logError $ "linkCheckThread error: " <> T.pack e
Right grs -> forM_ grs $ \(gInfo, gr) ->
unless (groupRemoved $ groupRegStatus gr) $
@@ -462,7 +462,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
getOwnerGroupMember :: GroupId -> GroupReg -> IO (Either String GroupMember)
getOwnerGroupMember gId GroupReg {dbOwnerMemberId} = case dbOwnerMemberId of
- Just mId -> withDB "getGroupMember" cc $ \db -> withExceptT show $ getGroupMember db (vr cc) user gId mId
+ Just mId -> withDB "getGroupMember" cc $ \db -> withExceptT show $ getGroupMember db (storeCxt cc) user gId mId
Nothing -> pure $ Left "no owner member in group registration"
deServiceJoinedGroup :: ContactId -> GroupInfo -> GroupMember -> IO ()
@@ -556,7 +556,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
Right (CRConnectionPlan _ _ (CPGroupLink (GLPKnown {groupInfo = g'}))) ->
case dbOwnerMemberId gr of
Just ownerGMId ->
- withDB "getGroupMember" cc (\db -> withExceptT show $ getGroupMember db (vr cc) user groupId ownerGMId) >>= \case
+ withDB "getGroupMember" cc (\db -> withExceptT show $ getGroupMember db (storeCxt cc) user groupId ownerGMId) >>= \case
Right ownerMember
| let GroupMember {memberRole = role} = ownerMember, role >= GROwner ->
setGroupStatus notifyAdminUsers st env cc groupId (GRSPendingApproval n') (`updatedNotification` g')
@@ -813,7 +813,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
_ -> False
checkValidOwner dbOwnerMemberId owners onValid = case dbOwnerMemberId of
Just ownerGMId ->
- withDB "checkGroupLink" cc (\db -> withExceptT show $ getGroupMember db (vr cc) user groupId ownerGMId) >>= \case
+ withDB "checkGroupLink" cc (\db -> withExceptT show $ getGroupMember db (storeCxt cc) user groupId ownerGMId) >>= \case
Right GroupMember {memberId, memberPubKey}
| any (\GroupLinkOwner {memberId = mId, memberKey} -> memberId == mId && memberPubKey == Just memberKey) owners -> onValid
_ -> setGroupStatus logError st env cc groupId GRSSuspendedBadRoles $ \gr' ->
@@ -970,6 +970,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
GLPConnectingProhibit _ -> sendMessage cc ct $ "Already connecting to this " <> gt <> "."
GLPConnectingConfirmReconnect -> sendMessage cc ct $ "Already connecting to this " <> gt <> "."
GLPNoRelays _ -> sendMessage cc ct $ T.toTitle gt <> " has no active relays. Please try again later."
+ GLPUpdateRequired _ -> sendMessage cc ct $ T.toTitle gt <> " requires a newer version."
GLPOwnLink _ -> sendMessage cc ct "Unexpected error. Please report it to directory admins."
_ -> sendMessage cc ct "Unexpected error. Please report it to directory admins."
@@ -984,7 +985,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
addGroupReg notifyAdminUsers st cc ct gInfo GRSProposed $ \_ -> pure ()
sendChatCmd cc (APIConnectPreparedGroup gId False (Just ownerContact) Nothing) >>= \case
Right CRStartedConnectionToGroup {groupInfo = gInfo'} ->
- withDB "getGroupMember" cc (\db -> withExceptT show $ getGroupMemberByMemberId db (vr cc) user gInfo' mId) >>= \case
+ withDB "getGroupMember" cc (\db -> withExceptT show $ getGroupMemberByMemberId db (storeCxt cc) user gInfo' mId) >>= \case
Right ownerMember ->
void $ setGroupRegOwner cc gId ownerMember
Left e -> do
@@ -997,7 +998,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
deReregistration ct g@GroupInfo {groupId, groupProfile = GroupProfile {publicGroup = pg_}} profileChanged LinkOwnerSig {ownerId = Just (B64UrlByteString oIdBytes)} = do
let mId = MemberId oIdBytes
gt = maybe "group" groupTypeStr' pg_
- withDB "getGroupMemberByMemberId" cc (\db -> withExceptT show $ getGroupMemberByMemberId db (vr cc) user g mId) >>= \case
+ withDB "getGroupMemberByMemberId" cc (\db -> withExceptT show $ getGroupMemberByMemberId db (storeCxt cc) user g mId) >>= \case
Right ownerMember@GroupMember {memberRole = role, memberStatus} ->
if
| role >= GROwner && memberStatus /= GSMemUnknown ->
@@ -1450,7 +1451,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
getOwnersInfo :: [(GroupInfo, GroupReg)] -> IO [((GroupInfo, GroupReg), Maybe (Either String Contact))]
getOwnersInfo gs =
fmap (either (\e -> map (,Just (Left e)) gs) id) $ withDB' "getOwnersInfo" cc $ \db ->
- mapM (\g@(_, gr) -> fmap ((g,) . Just . first show) $ runExceptT $ getContact db (vr cc) user $ dbContactId gr) gs
+ mapM (\g@(_, gr) -> fmap ((g,) . Just . first show) $ runExceptT $ getContact db (storeCxt cc) user $ dbContactId gr) gs
sendGroupsInfo :: Contact -> ChatItemId -> Bool -> ([(GroupInfo, GroupReg)], Int) -> IO ()
sendGroupsInfo ct ciId isAdmin (gs, n) = do
@@ -1518,7 +1519,7 @@ updateGroupListingFiles cc u dir =
Left e -> logError $ "generateListing error: failed to read groups: " <> T.pack e
getContact' :: ChatController -> User -> ContactId -> IO (Either String Contact)
-getContact' cc user ctId = withDB "getContact" cc $ \db -> withExceptT show $ getContact db (vr cc) user ctId
+getContact' cc user ctId = withDB "getContact" cc $ \db -> withExceptT show $ getContact db (storeCxt cc) user ctId
getGroupLink' :: ChatController -> User -> GroupInfo -> IO (Either String GroupLink)
getGroupLink' cc user gInfo =
diff --git a/apps/simplex-directory-service/src/Directory/Store.hs b/apps/simplex-directory-service/src/Directory/Store.hs
index b5f7220724..89c5178f7d 100644
--- a/apps/simplex-directory-service/src/Directory/Store.hs
+++ b/apps/simplex-directory-service/src/Directory/Store.hs
@@ -85,7 +85,6 @@ import Data.Time.Clock.System (systemEpochDay)
import Directory.Search
import Directory.Util
import Simplex.Chat.Controller
-import Simplex.Chat.Protocol (supportedChatVRange)
import Simplex.Chat.Options.DB (FromField (..), ToField (..))
import Simplex.Chat.Store
import Simplex.Chat.Store.Groups
@@ -314,43 +313,48 @@ getGroupReg_ db gId =
getGroupAndReg :: ChatController -> User -> GroupId -> IO (Either String (GroupInfo, GroupReg))
getGroupAndReg cc user@User {userId, userContactId} gId =
- withDB "getGroupAndReg" cc $ \db ->
- ExceptT $ firstRow (toGroupInfoReg (vr cc) user) ("group " ++ show gId ++ " not found") $
- DB.query db (groupReqQuery <> " AND g.group_id = ?") (userId, userContactId, gId)
+ withDB "getGroupAndReg" cc $ \db -> do
+ currentTs <- liftIO getCurrentTime
+ ExceptT $ firstRow (toGroupInfoReg currentTs (storeCxt cc) user) ("group " ++ show gId ++ " not found") $
+ DB.query db (groupReqQuery <> " AND g.group_id = ?") (userId, userContactId, gId)
getUserGroupReg :: ChatController -> User -> ContactId -> UserGroupRegId -> IO (Either String (GroupInfo, GroupReg))
getUserGroupReg cc user@User {userId, userContactId} ctId ugrId =
- withDB "getUserGroupReg" cc $ \db ->
- ExceptT $ firstRow (toGroupInfoReg (vr cc) user) ("group " ++ show ugrId ++ " not found") $
+ withDB "getUserGroupReg" cc $ \db -> do
+ currentTs <- liftIO getCurrentTime
+ ExceptT $ firstRow (toGroupInfoReg currentTs (storeCxt cc) user) ("group " ++ show ugrId ++ " not found") $
DB.query db (groupReqQuery <> " AND r.contact_id = ? AND r.user_group_reg_id = ?") (userId, userContactId, ctId, ugrId)
getUserGroupRegs :: ChatController -> User -> ContactId -> IO (Either String [(GroupInfo, GroupReg)])
getUserGroupRegs cc user@User {userId, userContactId} ctId =
- withDB' "getUserGroupRegs" cc $ \db ->
- map (toGroupInfoReg (vr cc) user)
+ withDB' "getUserGroupRegs" cc $ \db -> do
+ currentTs <- getCurrentTime
+ map (toGroupInfoReg currentTs (storeCxt cc) user)
<$> DB.query db (groupReqQuery <> " AND r.contact_id = ? ORDER BY r.user_group_reg_id") (userId, userContactId, ctId)
getAllListedGroups :: ChatController -> User -> IO (Either String [(GroupInfo, GroupReg, Maybe GroupLink)])
-getAllListedGroups cc user = withDB' "getAllListedGroups" cc $ \db -> getAllListedGroups_ db (vr cc) user
+getAllListedGroups cc user = withDB' "getAllListedGroups" cc $ \db -> getAllListedGroups_ db (storeCxt cc) user
-getAllListedGroups_ :: DB.Connection -> VersionRangeChat -> User -> IO [(GroupInfo, GroupReg, Maybe GroupLink)]
-getAllListedGroups_ db vr' user@User {userId, userContactId} =
+getAllListedGroups_ :: DB.Connection -> StoreCxt -> User -> IO [(GroupInfo, GroupReg, Maybe GroupLink)]
+getAllListedGroups_ db cxt user@User {userId, userContactId} = do
+ currentTs <- getCurrentTime
DB.query db (groupReqQuery <> " AND r.group_reg_status = ?") (userId, userContactId, GRSActive)
- >>= mapM (withGroupLink . toGroupInfoReg vr' user)
+ >>= mapM (withGroupLink . toGroupInfoReg currentTs cxt user)
where
withGroupLink (g, gr) = (g,gr,) . eitherToMaybe <$> runExceptT (getGroupLink db user g)
searchListedGroups :: ChatController -> User -> SearchType -> Maybe GroupId -> Int -> IO (Either String ([(GroupInfo, GroupReg)], Int))
searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pageSize =
- withDB' "searchListedGroups" cc $ \db ->
+ withDB' "searchListedGroups" cc $ \db -> do
+ currentTs <- getCurrentTime
case searchType of
STAll -> case lastGroup_ of
Nothing -> do
- gs <- groups $ DB.query db (listedGroupQuery <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, pageSize)
+ gs <- groups currentTs $ DB.query db (listedGroupQuery <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, pageSize)
n <- count $ DB.query db countQuery' (Only GRSActive)
pure (gs, n)
Just gId -> do
- gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, pageSize)
+ gs <- groups currentTs $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, pageSize)
n <- count $ DB.query db (countQuery' <> " AND r.group_id > ?") (GRSActive, gId)
pure (gs, n)
where
@@ -358,11 +362,11 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa
orderBy = " ORDER BY g.summary_current_members_count DESC, r.group_reg_id ASC "
STRecent -> case lastGroup_ of
Nothing -> do
- gs <- groups $ DB.query db (listedGroupQuery <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, pageSize)
+ gs <- groups currentTs $ DB.query db (listedGroupQuery <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, pageSize)
n <- count $ DB.query db countQuery' (Only GRSActive)
pure (gs, n)
Just gId -> do
- gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, pageSize)
+ gs <- groups currentTs $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, pageSize)
n <- count $ DB.query db (countQuery' <> " AND r.group_id > ?") (GRSActive, gId)
pure (gs, n)
where
@@ -370,11 +374,11 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa
orderBy = " ORDER BY r.created_at DESC, r.group_reg_id ASC "
STSearch search -> case lastGroup_ of
Nothing -> do
- gs <- groups $ DB.query db (listedGroupQuery <> searchCond <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, s, s, s, s, pageSize)
+ gs <- groups currentTs $ DB.query db (listedGroupQuery <> searchCond <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, s, s, s, s, pageSize)
n <- count $ DB.query db (countQuery' <> searchCond) (GRSActive, s, s, s, s)
pure (gs, n)
Just gId -> do
- gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> searchCond <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, s, s, s, s, pageSize)
+ gs <- groups currentTs $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> searchCond <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, s, s, s, s, pageSize)
n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> searchCond) (GRSActive, gId, s, s, s, s)
pure (gs, n)
where
@@ -382,7 +386,7 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa
countQuery' = countQuery <> " JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id WHERE r.group_reg_status = ? "
orderBy = " ORDER BY g.summary_current_members_count DESC, r.group_reg_id ASC "
where
- groups = (map (toGroupInfoReg (vr cc) user) <$>)
+ groups currentTs = (map (toGroupInfoReg currentTs (storeCxt cc) user) <$>)
count = maybeFirstRow' 0 fromOnly
listedGroupQuery = groupReqQuery <> " AND r.group_reg_status = ? "
countQuery = "SELECT COUNT(1) FROM groups g JOIN sx_directory_group_regs r ON g.group_id = r.group_id "
@@ -395,22 +399,25 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa
)
|]
-getAllGroupRegs_ :: DB.Connection -> User -> IO [(GroupInfo, GroupReg)]
-getAllGroupRegs_ db user@User {userId, userContactId} =
- map (toGroupInfoReg supportedChatVRange user)
+getAllGroupRegs_ :: DB.Connection -> StoreCxt -> User -> IO [(GroupInfo, GroupReg)]
+getAllGroupRegs_ db cxt user@User {userId, userContactId} = do
+ currentTs <- getCurrentTime
+ map (toGroupInfoReg currentTs cxt user)
<$> DB.query db groupReqQuery (userId, userContactId)
getDuplicateGroupRegs :: ChatController -> User -> Text -> IO (Either String [(GroupInfo, GroupReg)])
getDuplicateGroupRegs cc user@User {userId, userContactId} displayName =
- withDB' "getDuplicateGroupRegs" cc $ \db ->
- map (toGroupInfoReg (vr cc) user)
+ withDB' "getDuplicateGroupRegs" cc $ \db -> do
+ currentTs <- getCurrentTime
+ map (toGroupInfoReg currentTs (storeCxt cc) user)
<$> DB.query db (groupReqQuery <> " AND gp.display_name = ?") (userId, userContactId, displayName)
listLastGroups :: ChatController -> User -> Int -> IO (Either String ([(GroupInfo, GroupReg)], Int))
listLastGroups cc user@User {userId, userContactId} count =
withDB' "getUserGroupRegs" cc $ \db -> do
+ currentTs <- getCurrentTime
gs <-
- map (toGroupInfoReg (vr cc) user)
+ map (toGroupInfoReg currentTs (storeCxt cc) user)
<$> DB.query db (groupReqQuery <> " ORDER BY group_reg_id DESC LIMIT ?") (userId, userContactId, count)
n <- maybeFirstRow' 0 fromOnly $ DB.query_ db "SELECT COUNT(1) FROM sx_directory_group_regs"
pure (gs, n)
@@ -418,15 +425,16 @@ listLastGroups cc user@User {userId, userContactId} count =
listPendingGroups :: ChatController -> User -> Int -> IO (Either String ([(GroupInfo, GroupReg)], Int))
listPendingGroups cc user@User {userId, userContactId} count =
withDB' "getUserGroupRegs" cc $ \db -> do
+ currentTs <- getCurrentTime
gs <-
- map (toGroupInfoReg (vr cc) user)
+ map (toGroupInfoReg currentTs (storeCxt cc) user)
<$> DB.query db (groupReqQuery <> " AND r.group_reg_status LIKE 'pending_approval%' ORDER BY group_reg_id DESC LIMIT ?") (userId, userContactId, count)
n <- maybeFirstRow' 0 fromOnly $ DB.query_ db "SELECT COUNT(1) FROM sx_directory_group_regs WHERE group_reg_status LIKE 'pending_approval%'"
pure (gs, n)
-toGroupInfoReg :: VersionRangeChat -> User -> (GroupInfoRow :. GroupRegRow) -> (GroupInfo, GroupReg)
-toGroupInfoReg vr' User {userContactId} (groupRow :. grRow) =
- (toGroupInfo vr' userContactId [] groupRow, rowToGroupReg grRow)
+toGroupInfoReg :: UTCTime -> StoreCxt -> User -> (GroupInfoRow :. GroupRegRow) -> (GroupInfo, GroupReg)
+toGroupInfoReg currentTs cxt User {userContactId} (groupRow :. grRow) =
+ (toGroupInfo currentTs cxt userContactId [] groupRow, rowToGroupReg grRow)
type GroupRegRow = (GroupId, UserGroupRegId, ContactId, Maybe GroupMemberId, GroupRegStatus, BoolInt, UTCTime)
diff --git a/apps/simplex-directory-service/src/Directory/Store/Migrate.hs b/apps/simplex-directory-service/src/Directory/Store/Migrate.hs
index aa101d7bf7..d501fbd5c3 100644
--- a/apps/simplex-directory-service/src/Directory/Store/Migrate.hs
+++ b/apps/simplex-directory-service/src/Directory/Store/Migrate.hs
@@ -18,10 +18,9 @@ import Directory.Listing
import Directory.Options
import Directory.Store
import Simplex.Chat (createChatDatabase)
-import Simplex.Chat.Controller (ChatConfig (..), ChatDatabase (..))
+import Simplex.Chat.Controller (ChatConfig (..), ChatDatabase (..), mkStoreCxt)
import Simplex.Chat.Options (CoreChatOpts (..))
import Simplex.Chat.Options.DB
-import Simplex.Chat.Protocol (supportedChatVRange)
import Simplex.Chat.Store.Groups (getHostMember)
import Simplex.Chat.Store.Profiles (getUsers)
import Simplex.Chat.Store.Shared (getGroupInfo)
@@ -62,7 +61,7 @@ checkDirectoryLog opts cfg =
runDirectoryMigrations opts cfg st
gs <- readDirectoryLogData logFile
withActiveUser st $ \user -> withTransaction st $ \db -> do
- mapM_ (verifyGroupRegistration db user) gs
+ mapM_ (verifyGroupRegistration (mkStoreCxt cfg) db user) gs
putStrLn $ show (length gs) <> " group registrations OK"
importDirectoryLogToDB :: DirectoryOpts -> ChatConfig -> IO ()
@@ -73,7 +72,7 @@ importDirectoryLogToDB opts cfg = do
ctRegs <- TM.emptyIO
withActiveUser st $ \user -> withTransaction st $ \db -> do
forM_ gs $ \gr ->
- whenM (verifyGroupRegistration db user gr) $ do
+ whenM (verifyGroupRegistration (mkStoreCxt cfg) db user gr) $ do
putStrLn $ "importing group " <> show (dbGroupId gr)
insertGroupReg db =<< fixUserGroupRegId ctRegs gr
renamePath logFile (logFile ++ ".bak")
@@ -101,28 +100,28 @@ exportDBToDirectoryLog opts cfg =
runDirectoryMigrations opts cfg st
withActiveUser st $ \user -> do
gs <- withFile logFile WriteMode $ \h -> withTransaction st $ \db -> do
- gs <- getAllGroupRegs_ db user
+ gs <- getAllGroupRegs_ db (mkStoreCxt cfg) user
forM_ gs $ \(_, gr) ->
- whenM (verifyGroupRegistration db user gr) $
+ whenM (verifyGroupRegistration (mkStoreCxt cfg) db user gr) $
B.hPutStrLn h $ strEncode $ GRCreate gr
pure gs
putStrLn $ show (length gs) <> " group registrations exported"
saveGroupListingFiles :: DirectoryOpts -> ChatConfig -> IO ()
-saveGroupListingFiles opts _cfg = case webFolder opts of
+saveGroupListingFiles opts cfg = case webFolder opts of
Nothing -> exit "use --web-folder to generate listings"
Just dir ->
withChatStore opts $ \st -> withActiveUser st $ \user ->
withTransaction st $ \db ->
- getAllListedGroups_ db supportedChatVRange user >>= generateListing dir
+ getAllListedGroups_ db (mkStoreCxt cfg) user >>= generateListing dir
-verifyGroupRegistration :: DB.Connection -> User -> GroupReg -> IO Bool
-verifyGroupRegistration db user GroupReg {dbGroupId = gId, dbContactId = ctId, dbOwnerMemberId, groupRegStatus} =
- runExceptT (getGroupInfo db supportedChatVRange user gId) >>= \case
+verifyGroupRegistration :: StoreCxt -> DB.Connection -> User -> GroupReg -> IO Bool
+verifyGroupRegistration cxt db user GroupReg {dbGroupId = gId, dbContactId = ctId, dbOwnerMemberId, groupRegStatus} =
+ runExceptT (getGroupInfo db cxt user gId) >>= \case
Left e -> False <$ putStrLn ("Error: loading group " <> show gId <> " (skipping): " <> show e)
Right GroupInfo {localDisplayName} -> do
let groupRef = show gId <> " " <> T.unpack localDisplayName
- runExceptT (getHostMember db supportedChatVRange user gId) >>= \case
+ runExceptT (getHostMember db cxt user gId) >>= \case
Left e -> False <$ putStrLn ("Error: loading host member of group " <> groupRef <> " (skipping): " <> show e)
Right GroupMember {groupMemberId = mId', memberContactId = ctId'} -> case dbOwnerMemberId of
Nothing -> True <$ putStrLn ("Warning: group " <> groupRef <> " has no owner member ID, host member ID is " <> show mId' <> ", registration status: " <> B.unpack (strEncode groupRegStatus))
diff --git a/apps/simplex-directory-service/src/Directory/Util.hs b/apps/simplex-directory-service/src/Directory/Util.hs
index a4b79a1bef..52d376a945 100644
--- a/apps/simplex-directory-service/src/Directory/Util.hs
+++ b/apps/simplex-directory-service/src/Directory/Util.hs
@@ -15,9 +15,9 @@ import Simplex.Messaging.Agent.Store.Common (withTransaction)
import qualified Simplex.Messaging.Agent.Store.DB as DB
import Simplex.Messaging.Util (catchAll)
-vr :: ChatController -> VersionRangeChat
-vr ChatController {config = ChatConfig {chatVRange}} = chatVRange
-{-# INLINE vr #-}
+storeCxt :: ChatController -> StoreCxt
+storeCxt ChatController {config} = mkStoreCxt config
+{-# INLINE storeCxt #-}
withDB' :: Text -> ChatController -> (DB.Connection -> IO a) -> IO (Either String a)
withDB' cxt cc a = withDB cxt cc $ ExceptT . fmap Right . a
diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md
index b4edb9bd22..4f3df9d365 100644
--- a/bots/api/TYPES.md
+++ b/bots/api/TYPES.md
@@ -10,6 +10,10 @@ This file is generated automatically.
- [AgentCryptoError](#agentcryptoerror)
- [AgentErrorType](#agenterrortype)
- [AutoAccept](#autoaccept)
+- [BadgeInfo](#badgeinfo)
+- [BadgeProof](#badgeproof)
+- [BadgeStatus](#badgestatus)
+- [BadgeType](#badgetype)
- [BlockingInfo](#blockinginfo)
- [BlockingReason](#blockingreason)
- [BrokerErrorType](#brokererrortype)
@@ -122,6 +126,7 @@ This file is generated automatically.
- [LinkContent](#linkcontent)
- [LinkOwnerSig](#linkownersig)
- [LinkPreview](#linkpreview)
+- [LocalBadge](#localbadge)
- [LocalProfile](#localprofile)
- [MemberCriteria](#membercriteria)
- [MsgChatLink](#msgchatlink)
@@ -146,6 +151,7 @@ This file is generated automatically.
- [Profile](#profile)
- [ProxyClientError](#proxyclienterror)
- [ProxyError](#proxyerror)
+- [PublicGroupAccess](#publicgroupaccess)
- [PublicGroupData](#publicgroupdata)
- [PublicGroupProfile](#publicgroupprofile)
- [RCErrorType](#rcerrortype)
@@ -157,6 +163,7 @@ This file is generated automatically.
- [RcvFileTransfer](#rcvfiletransfer)
- [RcvGroupEvent](#rcvgroupevent)
- [RcvMsgError](#rcvmsgerror)
+- [RelayCapabilities](#relaycapabilities)
- [RelayProfile](#relayprofile)
- [RelayStatus](#relaystatus)
- [ReportReason](#reportreason)
@@ -165,6 +172,10 @@ This file is generated automatically.
- [SecurityCode](#securitycode)
- [SimplePreference](#simplepreference)
- [SimplexLinkType](#simplexlinktype)
+- [SimplexNameDomain](#simplexnamedomain)
+- [SimplexNameInfo](#simplexnameinfo)
+- [SimplexNameType](#simplexnametype)
+- [SimplexTLD](#simplextld)
- [SndCIStatusProgress](#sndcistatusprogress)
- [SndConnEvent](#sndconnevent)
- [SndError](#snderror)
@@ -347,6 +358,49 @@ INACTIVE:
- acceptIncognito: bool
+---
+
+## BadgeInfo
+
+**Record type**:
+- badgeType: [BadgeType](#badgetype)
+- badgeExpiry: UTCTime?
+- badgeExtra: string
+
+
+---
+
+## BadgeProof
+
+**Record type**:
+- badgeKeyIdx: int
+- presHeader: string
+- proof: string
+- badgeInfo: [BadgeInfo](#badgeinfo)
+
+
+---
+
+## BadgeStatus
+
+**Enum type**:
+- "active"
+- "expired"
+- "expiredOld"
+- "failed"
+- "unknownKey"
+
+
+---
+
+## BadgeType
+
+**Enum type**:
+- "supporter"
+- "legend"
+- "investor"
+
+
---
## BlockingInfo
@@ -992,9 +1046,6 @@ NoRcvFileUser:
UserUnknown:
- type: "userUnknown"
-ActiveUserExists:
-- type: "activeUserExists"
-
UserExists:
- type: "userExists"
- contactName: string
@@ -1760,6 +1811,7 @@ ContactViaAddress:
- profile: [Profile](#profile)
- message: [MsgContent](#msgcontent)?
- business: bool
+- localBadge: [LocalBadge](#localbadge)?
---
@@ -2091,6 +2143,10 @@ SimplexLink:
- simplexUri: string
- smpHosts: [string]
+SimplexName:
+- type: "simplexName"
+- nameInfo: [SimplexNameInfo](#simplexnameinfo)
+
Command:
- type: "command"
- commandStr: string
@@ -2331,6 +2387,10 @@ NoRelays:
- type: "noRelays"
- groupSLinkData_: [GroupShortLinkData](#groupshortlinkdata)?
+UpdateRequired:
+- type: "updateRequired"
+- groupSLinkData_: [GroupShortLinkData](#groupshortlinkdata)?
+
---
@@ -2487,6 +2547,7 @@ NoRelays:
- userChatRelay: [UserChatRelay](#userchatrelay)
- relayStatus: [RelayStatus](#relaystatus)
- relayLink: string?
+- relayCap: [RelayCapabilities](#relaycapabilities)
---
@@ -2657,6 +2718,15 @@ Unknown:
- content: [LinkContent](#linkcontent)?
+---
+
+## LocalBadge
+
+**Record type**:
+- badge: [BadgeInfo](#badgeinfo)
+- status: [BadgeStatus](#badgestatus)
+
+
---
## LocalProfile
@@ -2670,6 +2740,7 @@ Unknown:
- contactLink: string?
- preferences: [Preferences](#preferences)?
- peerType: [ChatPeerType](#chatpeertype)?
+- localBadge: [LocalBadge](#localbadge)?
- localAlias: string
@@ -2882,6 +2953,7 @@ SubscribeError:
- profile: [Profile](#profile)?
- pastTimestamp: bool
- userChatRelay: bool
+- clientService: bool
---
@@ -3014,6 +3086,7 @@ count=
- contactLink: string?
- preferences: [Preferences](#preferences)?
- peerType: [ChatPeerType](#chatpeertype)?
+- badge: [BadgeProof](#badgeproof)?
---
@@ -3056,6 +3129,17 @@ NO_SESSION:
- type: "NO_SESSION"
+---
+
+## PublicGroupAccess
+
+**Record type**:
+- groupWebPage: string?
+- groupDomain: string?
+- domainWebPage: bool
+- allowEmbedding: bool
+
+
---
## PublicGroupData
@@ -3072,6 +3156,7 @@ NO_SESSION:
- groupType: [GroupType](#grouptype)
- groupLink: string
- publicGroupId: string
+- publicGroupAccess: [PublicGroupAccess](#publicgroupaccess)?
---
@@ -3329,6 +3414,14 @@ ParseError:
- parseError: string
+---
+
+## RelayCapabilities
+
+**Record type**:
+- webDomain: string?
+
+
---
## RelayProfile
@@ -3436,6 +3529,44 @@ A_QUEUE:
- "relay"
+---
+
+## SimplexNameDomain
+
+**Record type**:
+- nameTLD: [SimplexTLD](#simplextld)
+- domain: string
+- subDomain: [string]
+
+
+---
+
+## SimplexNameInfo
+
+**Record type**:
+- nameType: [SimplexNameType](#simplexnametype)
+- nameDomain: [SimplexNameDomain](#simplexnamedomain)
+
+
+---
+
+## SimplexNameType
+
+**Enum type**:
+- "publicGroup"
+- "contact"
+
+
+---
+
+## SimplexTLD
+
+**Enum type**:
+- "simplex"
+- "testing"
+- "web"
+
+
---
## SndCIStatusProgress
@@ -4086,8 +4217,9 @@ Handshake:
- sendRcptsSmallGroups: bool
- autoAcceptMemberContacts: bool
- userMemberProfileUpdatedAt: UTCTime?
-- uiThemes: [UIThemeEntityOverrides](#uithemeentityoverrides)?
- userChatRelay: bool
+- clientService: bool
+- uiThemes: [UIThemeEntityOverrides](#uithemeentityoverrides)?
---
@@ -4140,7 +4272,7 @@ Handshake:
- cReqChatVRange: [VersionRange](#versionrange)
- localDisplayName: string
- profileId: int64
-- profile: [Profile](#profile)
+- profile: [LocalProfile](#localprofile)
- createdAt: UTCTime
- updatedAt: UTCTime
- xContactId: string?
diff --git a/bots/src/API/Docs/Commands.hs b/bots/src/API/Docs/Commands.hs
index 8894609758..8ebd510a55 100644
--- a/bots/src/API/Docs/Commands.hs
+++ b/bots/src/API/Docs/Commands.hs
@@ -202,6 +202,7 @@ cliCommands =
"AbortSwitchGroupMember",
"AcceptContact",
"AcceptMember",
+ "AddBadge",
"AddContact",
"AddMember",
"AllowRelayGroup",
@@ -271,6 +272,7 @@ cliCommands =
"SetAddressSettings",
"SetBotCommands",
"SetChatTTL",
+ "SetClientService",
"SetContactFeature",
"SetContactTimedMessages",
"SetGroupFeature",
@@ -279,6 +281,7 @@ cliCommands =
"SetGroupTimedMessages",
"SetLocalDeviceName",
"SetProfileAddress",
+ "SetPublicGroupAccess",
"SetSendReceipts",
"SetShowMemberMessages",
"SetShowMessages",
diff --git a/bots/src/API/Docs/Events.hs b/bots/src/API/Docs/Events.hs
index c8446e9e67..f0c9352efd 100644
--- a/bots/src/API/Docs/Events.hs
+++ b/bots/src/API/Docs/Events.hs
@@ -188,6 +188,7 @@ undocumentedEvents =
"CEvtCustomChatEvent",
"CEvtGroupMemberRatchetSync",
"CEvtGroupMemberSwitch",
+ "CEvtServiceSubStatus",
"CEvtNewRemoteHost",
"CEvtNoMemberContactCreating",
"CEvtNtfMessage",
diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs
index be4a55835a..7b268f4ec5 100644
--- a/bots/src/API/Docs/Types.hs
+++ b/bots/src/API/Docs/Types.hs
@@ -34,6 +34,7 @@ import Simplex.Chat.Store.Profiles
import Simplex.Chat.Store.Shared
import Simplex.Chat.Operators
import Simplex.Messaging.Agent.Store.Entity (DBStored (..))
+import Simplex.Chat.Badges (BadgeInfo (..), BadgeProof (..), BadgeStatus (..), BadgeType (..), JSONBadge (..))
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
@@ -183,6 +184,7 @@ ciQuoteType =
chatTypesDocsData :: [(SumTypeInfo, SumTypeJsonEncoding, String, [ConsName], Expr, Text)]
chatTypesDocsData =
[ ((sti @(Chat 'CTDirect)) {typeName = "AChat"}, STRecord, "", [], "", ""),
+ ((sti @JSONBadge) {typeName = "LocalBadge"}, STRecord, "", [], "", ""),
((sti @JSONChatInfo) {typeName = "ChatInfo"}, STUnion, "JCInfo", ["JCInfoInvalidJSON"], "", ""),
((sti @JSONCIContent) {typeName = "CIContent"}, STUnion, "JCI", ["JCIInvalidJSON"], "", ""),
((sti @JSONCIDeleted) {typeName = "CIDeleted"}, STUnion, "JCID", [], "", ""),
@@ -207,6 +209,7 @@ chatTypesDocsData =
(sti @AgentCryptoError, STUnion, "", ["RATCHET_EARLIER", "RATCHET_SKIPPED"], "", ""), -- TODO add fields to types
(sti @AgentErrorType, STUnion, "", [], "", ""),
(sti @AutoAccept, STRecord, "", [], "", ""),
+ (sti @BadgeProof, STRecord, "", [], "", ""),
(sti @BlockingInfo, STRecord, "", [], "", ""),
(sti @BlockingReason, STEnum, "BR", [], "", ""),
(sti @BrokerErrorType, STUnion, "", [], "", ""),
@@ -216,6 +219,8 @@ chatTypesDocsData =
(sti @ChatDeleteMode, STUnion, "CDM", [], Param "type" <> Choice "self" [("messages", "")] (OnOffParam "notify" "notify" (Just True)), ""),
(sti @ChatError, STUnion, "Chat", ["ChatErrorDatabase", "ChatErrorRemoteHost", "ChatErrorRemoteCtrl"], "", ""),
(sti @ChatErrorType, STUnion, "CE", ["CEContactNotFound", "CEServerProtocol", "CECallState", "CEInvalidChatMessage"], "", ""),
+ (sti @BadgeStatus, STEnum, "BS", [], "", ""),
+ (sti @BadgeType, STEnum, "BT", ["BTUnknown"], "", ""),
(sti @ChatFeature, STEnum, "CF", [], "", ""),
(sti @ChatItemDeletion, STRecord, "", [], "", "Message deletion result."),
(sti @ChatPeerType, STEnum, "CPT", [], "", ""),
@@ -303,6 +308,7 @@ chatTypesDocsData =
(sti @LinkContent, STUnion, "LC", [], "", ""),
(sti @LinkOwnerSig, STRecord, "", [], "", ""),
(sti @LinkPreview, STRecord, "", [], "", ""),
+ (sti @BadgeInfo, STRecord, "", [], "", ""),
(sti @LocalProfile, STRecord, "", [], "", ""),
(sti @MemberCriteria, STEnum1, "MC", [], "", ""),
(sti @MsgChatLink, STUnion, "MCL", [], "", "Connection link sent in a message - only short links are allowed."),
@@ -327,6 +333,7 @@ chatTypesDocsData =
(sti @Profile, STRecord, "", [], "", ""),
(sti @ProxyClientError, STUnion, "Proxy", [], "", ""),
(sti @ProxyError, STUnion, "", [], "", ""),
+ (sti @PublicGroupAccess, STRecord, "", [], "", ""),
(sti @PublicGroupData, STRecord, "", [], "", ""),
(sti @PublicGroupProfile, STRecord, "", [], "", ""),
(sti @RatchetSyncState, STEnum, "RS", [], "", ""),
@@ -338,6 +345,7 @@ chatTypesDocsData =
(sti @RcvFileTransfer, STRecord, "", [], "", ""),
(sti @RcvGroupEvent, STUnion, "RGE", [], "", ""),
(sti @RcvMsgError, STUnion, "RME", [], "", ""),
+ (sti @RelayCapabilities, STRecord, "", [], "", ""),
(sti @RelayProfile, STRecord, "", [], "", ""),
(sti @RelayStatus, STEnum, "RS", [], "", ""),
(sti @ReportReason, STEnum' (dropPfxSfx "RR" ""), "", ["RRUnknown"], "", ""),
@@ -345,6 +353,10 @@ chatTypesDocsData =
(sti @SecurityCode, STRecord, "", [], "", ""),
(sti @SimplePreference, STRecord, "", [], "", ""),
(sti @SimplexLinkType, STEnum, "XL", [], "", ""),
+ (sti @SimplexNameDomain, STRecord, "", [], "", ""),
+ (sti @SimplexNameInfo, STRecord, "", [], "", ""),
+ (sti @SimplexNameType, STEnum, "NT", [], "", ""),
+ (sti @SimplexTLD, STEnum, "TLD", [], "", ""),
(sti @SMPAgentError, STUnion, "", [], "", ""),
(sti @SndCIStatusProgress, STEnum, "SSP", [], "", ""),
(sti @SndConnEvent, STUnion, "SCE", [], "", ""),
@@ -416,11 +428,14 @@ deriving instance Generic AddressSettings
deriving instance Generic AgentCryptoError
deriving instance Generic AgentErrorType
deriving instance Generic AutoAccept
+deriving instance Generic BadgeProof
deriving instance Generic BlockingInfo
deriving instance Generic BlockingReason
deriving instance Generic BrokerErrorType
deriving instance Generic BusinessChatInfo
deriving instance Generic BusinessChatType
+deriving instance Generic BadgeStatus
+deriving instance Generic BadgeType
deriving instance Generic ChatBotCommand
deriving instance Generic ChatDeleteMode
deriving instance Generic ChatError
@@ -509,6 +524,7 @@ deriving instance Generic HandshakeError
deriving instance Generic InlineFileMode
deriving instance Generic InvitationLinkPlan
deriving instance Generic InvitedBy
+deriving instance Generic JSONBadge
deriving instance Generic JSONChatInfo
deriving instance Generic JSONCIContent
deriving instance Generic JSONCIDeleted
@@ -518,6 +534,7 @@ deriving instance Generic JSONCIStatus
deriving instance Generic LinkContent
deriving instance Generic LinkOwnerSig
deriving instance Generic LinkPreview
+deriving instance Generic BadgeInfo
deriving instance Generic LocalProfile
deriving instance Generic MemberCriteria
deriving instance Generic MsgChatLink
@@ -542,6 +559,7 @@ deriving instance Generic PreparedGroup
deriving instance Generic Profile
deriving instance Generic ProxyClientError
deriving instance Generic ProxyError
+deriving instance Generic PublicGroupAccess
deriving instance Generic PublicGroupData
deriving instance Generic PublicGroupProfile
deriving instance Generic RatchetSyncState
@@ -553,11 +571,16 @@ deriving instance Generic RcvFileStatus
deriving instance Generic RcvFileTransfer
deriving instance Generic RcvGroupEvent
deriving instance Generic RcvMsgError
+deriving instance Generic RelayCapabilities
deriving instance Generic RelayProfile
deriving instance Generic RelayStatus
deriving instance Generic ReportReason
deriving instance Generic SecurityCode
deriving instance Generic SimplexLinkType
+deriving instance Generic SimplexNameDomain
+deriving instance Generic SimplexNameInfo
+deriving instance Generic SimplexNameType
+deriving instance Generic SimplexTLD
deriving instance Generic SMPAgentError
deriving instance Generic SndCIStatusProgress
deriving instance Generic SndConnEvent
diff --git a/bots/src/API/TypeInfo.hs b/bots/src/API/TypeInfo.hs
index 36e87db62d..8dfba2bbb0 100644
--- a/bots/src/API/TypeInfo.hs
+++ b/bots/src/API/TypeInfo.hs
@@ -198,7 +198,11 @@ toTypeInfo tr =
"AgentInvId",
"AgentRcvFileId",
"AgentSndFileId",
+ "BadgeMasterKey",
"B64UrlByteString",
+ "BBSProof",
+ "BBSPresHeader",
+ "BBSSignature",
"CbNonce",
"ConnectionLink",
"ConnShortLink",
diff --git a/cabal.project b/cabal.project
index 7ee797e621..1c7d56ae17 100644
--- a/cabal.project
+++ b/cabal.project
@@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
- tag: f03cec7a58ed13a39a52886888c74bcefdb64479
+ tag: 8e0b8de529bcfee3d9c9a8c28b3a039a4644e9ae
source-repository-package
type: git
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
index b9218fbebc..73c050c106 100644
--- a/docs/SECURITY.md
+++ b/docs/SECURITY.md
@@ -1,7 +1,7 @@
---
title: Security Policy
permalink: /security/index.html
-revision: 23.04.2024
+revision: 25.05.2026
---
# Security Policy
@@ -12,7 +12,7 @@ The implementation security assessment of SimpleX cryptography and networking wa
The cryptographic review of SimpleX protocols design was done by Trail of Bits in [July 2024](../blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md).
-We are planning implementation security assessment in early 2025.
+We have scheduled implementation security assessment for June 2026.
## Reporting security issues
diff --git a/docs/rfcs/2026-05-21-public-namespaces.md b/docs/rfcs/2026-05-21-public-namespaces.md
new file mode 100644
index 0000000000..9f968945f3
--- /dev/null
+++ b/docs/rfcs/2026-05-21-public-namespaces.md
@@ -0,0 +1,246 @@
+# Public Namespaces for SimpleX Network
+
+## Motivation
+
+SimpleX has no user identifiers - users exchange invitation links out-of-band to connect. Short links help but are unmemorable. Public namespaces map human-readable names to SimpleX addresses.
+
+Names also solve censorship at two levels. A short link is controlled by one SMP router - that router can delete it. An on-chain name can't be deleted by any router. If the link is removed, the owner points the name to a new link on a different router. At the network level, links can be URL-filtered, but names resolve through SMP proxy chains - censoring a name requires controlling all resolvers the user can reach.
+
+DNS-based naming is vulnerable to domain seizure and requires WHOIS entries. Blockchains provide censorship-resistant globally unique names.
+
+## Product requirements
+
+### MVP
+
+- **Names**: TLD `.simplex` (e.g., `privacy.simplex`, `my-channel.simplex`). Subdomains: `support.acme.simplex`. In markdown, `.simplex` can be omitted: `#privacy` = `privacy.simplex`.
+- **Name rules**: see [Name rules](#name-rules).
+- **Two address types**: each name stores channel links (set) and contact links (set). Client uses the first; set provides forward-compatible redundancy. Either can be empty.
+- **Optional metadata**: admin SimpleX address, admin email.
+- **Registration**: commit-reveal to prevent frontrunning. Length-based ETH pricing. Annual renewal. Dutch auction on expiry.
+- **Launch gating**: requires SimpleX test NFT. Up to 5 paid + 5 test names per holder. Test names free, auto-removed after 3 months, use `testing` namespace.
+- **Reserved names**: common verticals (books, games, music, movies, news, etc.) reserved for community-operated channels managed by SimpleX Network Consortium.
+- Only 7+ character names can be registered during "launch phase".
+- **Resolution**: client queries two independent name servers (Ethereum light clients) via two SMP proxies. Agreement = trusted. Disagreement = warning.
+- **Double resolution**: name -> short link (on-chain), short link -> connection data (existing protocol).
+- **Verification**: if on-chain link matches profile address, name is verified. Manual "verify" button + optional auto-verify on profile open.
+- **Markdown**: `#name` (`.simplex` implied), `#name.simplex` (explicit), `#name.testing` for test namespace. In CLI, `#` is local in group commands, global in `/c` and message bodies.
+- **Search**: `#name.simplex` auto-resolves. Disable in "More privacy" settings.
+- **Router role**: `names` added to `ServerRoles`. Not all routers support it.
+- **Contract**: ENS fork on Ethereum mainnet. ETH payment. Upgradeable.
+
+### Post-MVP
+
+- **Multiple links**: redundant entries per name. Forward-compatible schema in MVP where practical.
+- **Contact syntax**: `:name.simplex`, `:my-name.simplex`. Same namespace, different link type. MVP parser supports this syntax; resolution works; UI support is post-MVP.
+- **Community Credits**: replace ETH for private registration.
+- **Unicode expansion**: add scripts as user base grows.
+
+## Part 1: Blockchain contract
+
+### Overview
+
+ENS fork on Ethereum mainnet. Retains commit-reveal, pricing, expiry, Dutch auction. Compatible with ENS dApp. Upgradeable.
+
+ENS source:
+- Contracts: https://github.com/ensdomains/ens-contracts
+- dApp: https://github.com/ensdomains/ens-app-v3
+- JS library: https://github.com/ensdomains/ensjs
+
+### Contract state
+
+```
+Name record (ENS structure + SimpleX resolver fields):
+ owner : address
+ channelLinks : string[]
+ contactLinks : string[]
+ adminAddress : string -- optional
+ adminEmail : string -- optional
+ expiry : uint256
+ isTest : bool
+
+Global state:
+ reservedNames : mapping(string => bool)
+ testNFT : address
+ registrationLimit : uint8 -- 5
+ testLimit : uint8 -- 5
+```
+
+There must be maps to track names by owner, but specific contract design should be based on ENS.
+
+### Name rules
+
+ENS normalization (ENSIP-15) with additional restrictions enforced in dApp (registration) and resolvers (resolution). Contract follows ENS as-is.
+
+Additional restrictions beyond ENSIP-15:
+- No consecutive hyphens.
+- No accented characters. Latin is `a-z` only (same as DNS LDH rule).
+- Allowed scripts: Latin, Cyrillic, Arabic, Hebrew, Devanagari, Bengali, Thai, Greek, CJK, Hangul, Kana. Expandable as user base grows.
+
+### Registration flow
+
+1. NFT check
+2. Limit check (5 paid / 5 test)
+3. `commit(hash(name, owner, secret))`
+4. Wait (min 1 minute)
+5. `reveal(name, owner, secret)` + ETH (zero for test)
+6. Validate: well-formed, not taken, not reserved, fee covered
+7. Store record
+
+### Pricing
+
+Annual fees by name length:
+
+| Length | Fee |
+|---|---|
+| 7+ | base |
+| 6 | 4x |
+| 5 | 16x |
+| 4 | 64x |
+| 3 | 256x |
+
+Test names: free, expire after 3 months.
+
+### Renewal and expiry
+
+Annual renewal. Grace period, then Dutch auction decaying to base price.
+
+### Updates
+
+Owner can update links, admin address, admin email. Transfer follows ENS mechanics.
+
+### Reserved names
+
+List for community channels (e.g., `books`, `games`, `music`, `news`):
+- Not registrable by users
+- Revenue shared with network
+
+### Retained ENS features
+
+- **Resolver pattern**: registry maps name -> (owner, resolver). A SimpleX Resolver contract stores channel links, contact links, admin fields. Allows future extensibility without registry changes.
+- **Multicoin address records**: BTC/ETH/XMR donation addresses per name. Subscribers see donation options from name resolution.
+- **Text records**: generic key-value store for future metadata without contract upgrades.
+- **Reverse resolution**: name lookup by address. Enables verification and discovery.
+- **Subdomain registrar**: owner of `acme.simplex` can create `support.acme.simplex`, `sales.acme.simplex` without additional on-chain registration.
+
+### Removed ENS features
+
+- Avatar/image records.
+- `.eth` TLD and ENS name imports.
+- DNS name registration (DNSSEC imports).
+
+### Governance
+
+SimpleX Chat during testing and launch phases, migration to SimpleX Network Consortium.
+
+## Part 2: SMP protocol extension
+
+### New router role
+
+```haskell
+data ServerRoles = ServerRoles
+ { storage :: Bool,
+ proxy :: Bool,
+ names :: Bool
+ }
+```
+
+Name-capable routers run an Ethereum light client.
+
+### Resolution protocol
+
+Uses existing SMP proxy infrastructure. Client sends queries through a proxy, not directly to name servers.
+
+#### Commands
+
+```
+Client -> Proxy -> Name Server:
+ RSLV
+
+Name Server -> Proxy -> Client:
+ NAME
+ ERR AUTH
+```
+
+Forwarded via `PRXY`/`PFWD`/`RRES` mechanism.
+
+#### Two-operator resolution
+
+```
+Client -> Proxy A (Op 1) -> Name Server X (Op 1)
+Client -> Proxy B (Op 2) -> Name Server Y (Op 2)
+```
+
+Both read same Ethereum state.
+
+- Agree: trusted
+- Disagree: warn, don't use
+- One fails: retry with another server or show single result with reduced trust
+
+Proxy sees client IP and session, but not query. Name server sees query, not client IP or session.
+
+#### Name server implementation
+
+1. Runs Ethereum light client (e.g., Helios) tracking SNRC
+2. Receives `RSLV` via SMP proxy
+3. Returns record from local state
+
+State proofs can be added post-MVP.
+
+#### Configuration
+
+```haskell
+data NamesConfig = NamesConfig
+ { ethereumEndpoint :: String,
+ snrcAddress :: EthAddress,
+ cacheSeconds :: Int
+ }
+```
+
+#### Versioning
+
+New SMP protocol version. Older routers/clients don't advertise the capability.
+
+### Default routers
+
+Default router list updated to include name-capable routers.
+
+## Part 3: UI integration
+
+### Markdown
+
+- `#name` or `#name.simplex` - native names (no dot = `.simplex` implied)
+- `#my-name` or `#my-name.simplex` - hyphenated names
+- `#sub.name.simplex` - subdomains (explicit TLD)
+- `#name.testing` - test namespace
+- Rendered as clickable resolve-and-connect links
+
+CLI: `#` = local in group commands, global in `/c` and messages.
+
+`:name.simplex`, `:my-name.simplex` - contact addresses (same namespace, different link type). MVP parser supports this syntax; resolution works; UI support is post-MVP.
+
+### Resolution flow
+
+1. Normalize per ENSIP-15, compute namehash
+2. `RSLV` to two name servers via two proxies
+3. Compare results
+4. First channel link -> short link resolution -> connection data
+5. Present for joining
+
+### Search
+
+`#...simplex` triggers resolution. Disable in "More privacy" settings.
+
+### Verification
+
+On-chain link matches profile address = verified. Only name owner can set on-chain links.
+
+- Manual: "Verify" button resolves and compares
+- Auto: optional setting, resolves on profile open
+
+### Display
+
+Show name and verification status. `#` is syntax, not part of the name.
+
+## Open questions
+
+1. **Contract upgrade mechanism**: proxy pattern with timelock? Migration path for future Community Credits payment and domain name support.
diff --git a/flake.nix b/flake.nix
index 9c145275c0..a56a88e837 100644
--- a/flake.nix
+++ b/flake.nix
@@ -329,6 +329,7 @@
packages.simplex-chat.flags.swift = true;
packages.simplexmq.flags.swift = true;
packages.direct-sqlcipher.flags.commoncrypto = true;
+ packages.simplexmq.flags.commoncrypto = true;
packages.entropy.flags.DoNotGetEntropy = true;
packages.simplex-chat.flags.client_library = true;
packages.simplexmq.flags.client_library = true;
@@ -344,6 +345,7 @@
pkgs' = pkgs;
extra-modules = [{
packages.direct-sqlcipher.flags.commoncrypto = true;
+ packages.simplexmq.flags.commoncrypto = true;
packages.entropy.flags.DoNotGetEntropy = true;
packages.simplex-chat.flags.client_library = true;
packages.simplexmq.flags.client_library = true;
@@ -363,6 +365,7 @@
packages.simplex-chat.flags.swift = true;
packages.simplexmq.flags.swift = true;
packages.direct-sqlcipher.flags.commoncrypto = true;
+ packages.simplexmq.flags.commoncrypto = true;
packages.entropy.flags.DoNotGetEntropy = true;
packages.simplex-chat.flags.client_library = true;
packages.simplexmq.flags.client_library = true;
@@ -378,6 +381,7 @@
pkgs' = pkgs;
extra-modules = [{
packages.direct-sqlcipher.flags.commoncrypto = true;
+ packages.simplexmq.flags.commoncrypto = true;
packages.entropy.flags.DoNotGetEntropy = true;
packages.simplex-chat.flags.client_library = true;
packages.simplexmq.flags.client_library = true;
diff --git a/libsimplex.dll.def b/libsimplex.dll.def
index 76e6f9f3ee..ec4125193f 100644
--- a/libsimplex.dll.def
+++ b/libsimplex.dll.def
@@ -16,6 +16,8 @@ EXPORTS
chat_password_hash
chat_valid_name
chat_json_length
+ chat_badge_keygen
+ chat_badge_issue
chat_encrypt_media
chat_decrypt_media
chat_write_file
diff --git a/packages/simplex-chat-client/types/typescript/package.json b/packages/simplex-chat-client/types/typescript/package.json
index c929125033..756e181307 100644
--- a/packages/simplex-chat-client/types/typescript/package.json
+++ b/packages/simplex-chat-client/types/typescript/package.json
@@ -1,6 +1,6 @@
{
"name": "@simplex-chat/types",
- "version": "0.7.0",
+ "version": "0.9.0",
"description": "TypeScript types for SimpleX Chat bot libraries",
"main": "dist/index.js",
"types": "dist/index.d.ts",
diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts
index 7e618e05c8..597f2f5503 100644
--- a/packages/simplex-chat-client/types/typescript/src/types.ts
+++ b/packages/simplex-chat-client/types/typescript/src/types.ts
@@ -186,6 +186,33 @@ export interface AutoAccept {
acceptIncognito: boolean
}
+export interface BadgeInfo {
+ badgeType: BadgeType
+ badgeExpiry?: string // ISO-8601 timestamp
+ badgeExtra: string
+}
+
+export interface BadgeProof {
+ badgeKeyIdx: number // int
+ presHeader: string
+ proof: string
+ badgeInfo: BadgeInfo
+}
+
+export enum BadgeStatus {
+ Active = "active",
+ Expired = "expired",
+ ExpiredOld = "expiredOld",
+ Failed = "failed",
+ UnknownKey = "unknownKey",
+}
+
+export enum BadgeType {
+ Supporter = "supporter",
+ Legend = "legend",
+ Investor = "investor",
+}
+
export interface BlockingInfo {
reason: BlockingReason
notice?: ClientNotice
@@ -994,7 +1021,6 @@ export type ChatErrorType =
| ChatErrorType.NoSndFileUser
| ChatErrorType.NoRcvFileUser
| ChatErrorType.UserUnknown
- | ChatErrorType.ActiveUserExists
| ChatErrorType.UserExists
| ChatErrorType.ChatRelayExists
| ChatErrorType.DifferentActiveUser
@@ -1072,7 +1098,6 @@ export namespace ChatErrorType {
| "noSndFileUser"
| "noRcvFileUser"
| "userUnknown"
- | "activeUserExists"
| "userExists"
| "chatRelayExists"
| "differentActiveUser"
@@ -1170,10 +1195,6 @@ export namespace ChatErrorType {
type: "userUnknown"
}
- export interface ActiveUserExists extends Interface {
- type: "activeUserExists"
- }
-
export interface UserExists extends Interface {
type: "userExists"
contactName: string
@@ -2044,6 +2065,7 @@ export interface ContactShortLinkData {
profile: Profile
message?: MsgContent
business: boolean
+ localBadge?: LocalBadge
}
export enum ContactStatus {
@@ -2358,6 +2380,7 @@ export type Format =
| Format.Uri
| Format.HyperLink
| Format.SimplexLink
+ | Format.SimplexName
| Format.Command
| Format.Mention
| Format.Email
@@ -2375,6 +2398,7 @@ export namespace Format {
| "uri"
| "hyperLink"
| "simplexLink"
+ | "simplexName"
| "command"
| "mention"
| "email"
@@ -2431,6 +2455,11 @@ export namespace Format {
smpHosts: string[] // non-empty
}
+ export interface SimplexName extends Interface {
+ type: "simplexName"
+ nameInfo: SimplexNameInfo
+ }
+
export interface Command extends Interface {
type: "command"
commandStr: string
@@ -2602,6 +2631,7 @@ export type GroupLinkPlan =
| GroupLinkPlan.ConnectingProhibit
| GroupLinkPlan.Known
| GroupLinkPlan.NoRelays
+ | GroupLinkPlan.UpdateRequired
export namespace GroupLinkPlan {
export type Tag =
@@ -2611,6 +2641,7 @@ export namespace GroupLinkPlan {
| "connectingProhibit"
| "known"
| "noRelays"
+ | "updateRequired"
interface Interface {
type: Tag
@@ -2649,6 +2680,11 @@ export namespace GroupLinkPlan {
type: "noRelays"
groupSLinkData_?: GroupShortLinkData
}
+
+ export interface UpdateRequired extends Interface {
+ type: "updateRequired"
+ groupSLinkData_?: GroupShortLinkData
+ }
}
export interface GroupMember {
@@ -2762,6 +2798,7 @@ export interface GroupRelay {
userChatRelay: UserChatRelay
relayStatus: RelayStatus
relayLink?: string
+ relayCap: RelayCapabilities
}
export type GroupRootKey = GroupRootKey.Private | GroupRootKey.Public
@@ -2925,6 +2962,11 @@ export interface LinkPreview {
content?: LinkContent
}
+export interface LocalBadge {
+ badge: BadgeInfo
+ status: BadgeStatus
+}
+
export interface LocalProfile {
profileId: number // int64
displayName: string
@@ -2934,6 +2976,7 @@ export interface LocalProfile {
contactLink?: string
preferences?: Preferences
peerType?: ChatPeerType
+ localBadge?: LocalBadge
localAlias: string
}
@@ -3181,6 +3224,7 @@ export interface NewUser {
profile?: Profile
pastTimestamp: boolean
userChatRelay: boolean
+ clientService: boolean
}
export interface NoteFolder {
@@ -3284,6 +3328,7 @@ export interface Profile {
contactLink?: string
preferences?: Preferences
peerType?: ChatPeerType
+ badge?: BadgeProof
}
export type ProxyClientError =
@@ -3342,6 +3387,13 @@ export namespace ProxyError {
}
}
+export interface PublicGroupAccess {
+ groupWebPage?: string
+ groupDomain?: string
+ domainWebPage: boolean
+ allowEmbedding: boolean
+}
+
export interface PublicGroupData {
publicMemberCount: number // int64
}
@@ -3350,6 +3402,7 @@ export interface PublicGroupProfile {
groupType: GroupType
groupLink: string
publicGroupId: string
+ publicGroupAccess?: PublicGroupAccess
}
export type RCErrorType =
@@ -3738,6 +3791,10 @@ export namespace RcvMsgError {
}
}
+export interface RelayCapabilities {
+ webDomain?: string
+}
+
export interface RelayProfile {
displayName: string
fullName: string
@@ -3841,6 +3898,28 @@ export enum SimplexLinkType {
Relay = "relay",
}
+export interface SimplexNameDomain {
+ nameTLD: SimplexTLD
+ domain: string
+ subDomain: string[]
+}
+
+export interface SimplexNameInfo {
+ nameType: SimplexNameType
+ nameDomain: SimplexNameDomain
+}
+
+export enum SimplexNameType {
+ PublicGroup = "publicGroup",
+ Contact = "contact",
+}
+
+export enum SimplexTLD {
+ Simplex = "simplex",
+ Testing = "testing",
+ Web = "web",
+}
+
export enum SndCIStatusProgress {
Partial = "partial",
Complete = "complete",
@@ -4795,8 +4874,9 @@ export interface User {
sendRcptsSmallGroups: boolean
autoAcceptMemberContacts: boolean
userMemberProfileUpdatedAt?: string // ISO-8601 timestamp
- uiThemes?: UIThemeEntityOverrides
userChatRelay: boolean
+ clientService: boolean
+ uiThemes?: UIThemeEntityOverrides
}
export interface UserChatRelay {
@@ -4833,7 +4913,7 @@ export interface UserContactRequest {
cReqChatVRange: VersionRange
localDisplayName: string
profileId: number // int64
- profile: Profile
+ profile: LocalProfile
createdAt: string // ISO-8601 timestamp
updatedAt: string // ISO-8601 timestamp
xContactId?: string
diff --git a/packages/simplex-chat-nodejs/package.json b/packages/simplex-chat-nodejs/package.json
index 5166283e75..40ef106103 100644
--- a/packages/simplex-chat-nodejs/package.json
+++ b/packages/simplex-chat-nodejs/package.json
@@ -1,6 +1,6 @@
{
"name": "simplex-chat",
- "version": "6.5.2",
+ "version": "6.5.5",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
@@ -24,7 +24,7 @@
"docs": "typedoc"
},
"dependencies": {
- "@simplex-chat/types": "^0.7.0",
+ "@simplex-chat/types": "^0.9.0",
"extract-zip": "^2.0.1",
"fast-deep-equal": "^3.1.3",
"node-addon-api": "^8.5.0"
diff --git a/packages/simplex-chat-nodejs/src/download-libs.js b/packages/simplex-chat-nodejs/src/download-libs.js
index db042d48a2..4fad0f45a9 100644
--- a/packages/simplex-chat-nodejs/src/download-libs.js
+++ b/packages/simplex-chat-nodejs/src/download-libs.js
@@ -4,7 +4,7 @@ const path = require('path');
const extract = require('extract-zip');
const GITHUB_REPO = 'simplex-chat/simplex-chat-libs';
-const RELEASE_TAG = 'v6.5.2';
+const RELEASE_TAG = 'v6.5.5';
const BACKEND = (process.env.SIMPLEX_BACKEND || process.env.npm_config_simplex_backend || 'sqlite').toLowerCase();
if (BACKEND !== 'sqlite' && BACKEND !== 'postgres') {
diff --git a/packages/simplex-chat-python/examples/squaring_bot.py b/packages/simplex-chat-python/examples/squaring_bot.py
index 296b51347e..4d062ad718 100644
--- a/packages/simplex-chat-python/examples/squaring_bot.py
+++ b/packages/simplex-chat-python/examples/squaring_bot.py
@@ -26,7 +26,15 @@ bot = Bot(
profile=BotProfile(display_name="Squaring bot"),
db=SqliteDb(file_prefix="./squaring_bot"),
welcome="Send me a number, I'll square it.",
- commands=[BotCommand(keyword="help", label="Show help")],
+ commands=[
+ # `params=None` (default): the client SENDS `/help` immediately
+ # when the user taps it in the commands menu.
+ BotCommand(keyword="help", label="Show help"),
+ # `params=""`: the client PASTES `/square `
+ # into the input box and positions the cursor at the end. The
+ # user replaces `` with the actual number and sends.
+ BotCommand(keyword="square", label="Square a number", params=""),
+ ],
)
NUMBER_RE = re.compile(r"^-?\d+(\.\d+)?$")
@@ -48,5 +56,19 @@ async def help_cmd(msg: Message, _cmd: ParsedCommand) -> None:
await msg.reply("Send a number, I'll square it.")
+@bot.on_command("square")
+async def square_cmd(msg: Message, cmd: ParsedCommand) -> None:
+ """Demonstrates the `params` flow: `cmd.args` is the trimmed text
+ AFTER `/square`. When the user tapped the menu entry above, the
+ client pasted `/square ` and the user replaced
+ `` with the actual value before sending."""
+ try:
+ n = float(cmd.args)
+ except ValueError:
+ await msg.reply(f"Usage: /square (got {cmd.args!r})")
+ return
+ await msg.reply(f"{n} * {n} = {n * n}")
+
+
if __name__ == "__main__":
bot.run()
diff --git a/packages/simplex-chat-python/src/simplex_chat/_version.py b/packages/simplex-chat-python/src/simplex_chat/_version.py
index bd182d0240..77acd0e8b3 100644
--- a/packages/simplex-chat-python/src/simplex_chat/_version.py
+++ b/packages/simplex-chat-python/src/simplex_chat/_version.py
@@ -5,5 +5,5 @@ Bump both together for normal releases. For wrapper-only fixes use a PEP 440
post-release: __version__ = "6.5.2.post1", LIBS_VERSION unchanged.
"""
-__version__ = "6.5.2" # PEP 440 — read by hatchling for wheel metadata
-LIBS_VERSION = "6.5.2" # simplex-chat-libs release tag (no 'v' prefix)
+__version__ = "6.5.5" # PEP 440 — read by hatchling for wheel metadata
+LIBS_VERSION = "6.5.5" # simplex-chat-libs release tag (no 'v' prefix)
diff --git a/packages/simplex-chat-python/src/simplex_chat/bot.py b/packages/simplex-chat-python/src/simplex_chat/bot.py
index fb511e2818..4e385493b2 100644
--- a/packages/simplex-chat-python/src/simplex_chat/bot.py
+++ b/packages/simplex-chat-python/src/simplex_chat/bot.py
@@ -33,8 +33,38 @@ from .types import T
@dataclass(slots=True)
class BotCommand:
+ """One entry in the bot's advertised slash-command list (wire-side
+ `groupPreferences.commands` or profile `preferences.commands`).
+
+ `keyword` and `label` are required: `keyword` is what the user
+ types after `/`; `label` is the human-readable description shown
+ next to the keyword in the SimpleX client's commands menu.
+
+ `params` is an optional placeholder string that controls how the
+ client behaves when the user taps the command in the menu:
+
+ * `params=None` (default) — the client SENDS `/`
+ immediately on tap; no input-box detour. Use this for
+ zero-argument commands (`/help`, `/ping`) where the action is
+ unambiguous.
+
+ * `params=""` — the client PASTES `/`
+ into the input box and positions the cursor at the end. The
+ user edits the placeholder and sends. Use this for commands
+ that take a required argument (`/review `,
+ `/order `) so the user sees the expected shape
+ without having to remember it.
+
+ Mirrors `CBCCommand` in the Haskell core
+ (`Simplex.Chat.Types.Preferences`) and the wire TypedDict
+ `ChatBotCommand_command`. Both SimpleX clients (Android/Kotlin
+ and iOS/Swift) implement the paste-vs-send branch on the
+ `params` field; see `CommandsMenuView.{kt,swift}` for the
+ reference UI behaviour.
+ """
keyword: str
label: str
+ params: str | None = None
class Bot(Client):
@@ -145,10 +175,24 @@ class Bot(Client):
"files": {"allow": "yes" if self._allow_files else "no"},
}
if self._commands:
- prefs["commands"] = [
- {"type": "command", "keyword": c.keyword, "label": c.label}
- for c in self._commands
- ]
+ cmds: list[T.ChatBotCommand] = []
+ for c in self._commands:
+ entry: T.ChatBotCommand_command = {
+ "type": "command",
+ "keyword": c.keyword,
+ "label": c.label,
+ }
+ # `params` is `NotRequired[str]` on the wire; omit the
+ # key entirely when None so the Haskell parser sees
+ # `Nothing` rather than `Just ""`. The two have
+ # different client semantics: `Nothing` (`params=None`)
+ # triggers an immediate send on tap; `Just ""` would
+ # paste `/ ` (with a trailing space) into the
+ # input box, which is rarely what the operator wants.
+ if c.params is not None:
+ entry["params"] = c.params
+ cmds.append(entry)
+ prefs["commands"] = cmds
p["preferences"] = prefs
p["peerType"] = "bot"
return p
diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_types.py b/packages/simplex-chat-python/src/simplex_chat/types/_types.py
index b2fc00a44c..9546bfe5a8 100644
--- a/packages/simplex-chat-python/src/simplex_chat/types/_types.py
+++ b/packages/simplex-chat-python/src/simplex_chat/types/_types.py
@@ -138,6 +138,21 @@ AgentErrorType_Tag = Literal["CMD", "CONN", "NO_USER", "SMP", "NTF", "XFTP", "FI
class AutoAccept(TypedDict):
acceptIncognito: bool
+class BadgeInfo(TypedDict):
+ badgeType: "BadgeType"
+ badgeExpiry: NotRequired[str] # ISO-8601 timestamp
+ badgeExtra: str
+
+class BadgeProof(TypedDict):
+ badgeKeyIdx: int # int
+ presHeader: str
+ proof: str
+ badgeInfo: "BadgeInfo"
+
+BadgeStatus = Literal["active", "expired", "expiredOld", "failed", "unknownKey"]
+
+BadgeType = Literal["supporter", "legend", "investor"]
+
class BlockingInfo(TypedDict):
reason: "BlockingReason"
notice: NotRequired["ClientNotice"]
@@ -712,9 +727,6 @@ class ChatErrorType_noRcvFileUser(TypedDict):
class ChatErrorType_userUnknown(TypedDict):
type: Literal["userUnknown"]
-class ChatErrorType_activeUserExists(TypedDict):
- type: Literal["activeUserExists"]
-
class ChatErrorType_userExists(TypedDict):
type: Literal["userExists"]
contactName: str
@@ -987,7 +999,6 @@ ChatErrorType = (
| ChatErrorType_noSndFileUser
| ChatErrorType_noRcvFileUser
| ChatErrorType_userUnknown
- | ChatErrorType_activeUserExists
| ChatErrorType_userExists
| ChatErrorType_chatRelayExists
| ChatErrorType_differentActiveUser
@@ -1059,7 +1070,7 @@ ChatErrorType = (
| ChatErrorType_exception
)
-ChatErrorType_Tag = Literal["noActiveUser", "noConnectionUser", "noSndFileUser", "noRcvFileUser", "userUnknown", "activeUserExists", "userExists", "chatRelayExists", "differentActiveUser", "cantDeleteActiveUser", "cantDeleteLastUser", "cantHideLastUser", "hiddenUserAlwaysMuted", "emptyUserPassword", "userAlreadyHidden", "userNotHidden", "invalidDisplayName", "chatNotStarted", "chatNotStopped", "chatStoreChanged", "invalidConnReq", "unsupportedConnReq", "connReqMessageProhibited", "contactNotReady", "contactNotActive", "contactDisabled", "connectionDisabled", "groupUserRole", "groupMemberInitialRole", "contactIncognitoCantInvite", "groupIncognitoCantInvite", "groupContactRole", "groupDuplicateMember", "groupDuplicateMemberId", "groupNotJoined", "groupMemberNotActive", "cantBlockMemberForSelf", "groupMemberUserRemoved", "groupMemberNotFound", "groupCantResendInvitation", "groupInternal", "fileNotFound", "fileSize", "fileAlreadyReceiving", "fileCancelled", "fileCancel", "fileAlreadyExists", "fileWrite", "fileSend", "fileRcvChunk", "fileInternal", "fileImageType", "fileImageSize", "fileNotReceived", "fileNotApproved", "fallbackToSMPProhibited", "inlineFileProhibited", "invalidForward", "invalidChatItemUpdate", "invalidChatItemDelete", "hasCurrentCall", "noCurrentCall", "callContact", "directMessagesProhibited", "agentVersion", "agentNoSubResult", "commandError", "agentCommandError", "invalidFileDescription", "connectionIncognitoChangeProhibited", "connectionUserChangeProhibited", "peerChatVRangeIncompatible", "relayTestError", "internalError", "exception"]
+ChatErrorType_Tag = Literal["noActiveUser", "noConnectionUser", "noSndFileUser", "noRcvFileUser", "userUnknown", "userExists", "chatRelayExists", "differentActiveUser", "cantDeleteActiveUser", "cantDeleteLastUser", "cantHideLastUser", "hiddenUserAlwaysMuted", "emptyUserPassword", "userAlreadyHidden", "userNotHidden", "invalidDisplayName", "chatNotStarted", "chatNotStopped", "chatStoreChanged", "invalidConnReq", "unsupportedConnReq", "connReqMessageProhibited", "contactNotReady", "contactNotActive", "contactDisabled", "connectionDisabled", "groupUserRole", "groupMemberInitialRole", "contactIncognitoCantInvite", "groupIncognitoCantInvite", "groupContactRole", "groupDuplicateMember", "groupDuplicateMemberId", "groupNotJoined", "groupMemberNotActive", "cantBlockMemberForSelf", "groupMemberUserRemoved", "groupMemberNotFound", "groupCantResendInvitation", "groupInternal", "fileNotFound", "fileSize", "fileAlreadyReceiving", "fileCancelled", "fileCancel", "fileAlreadyExists", "fileWrite", "fileSend", "fileRcvChunk", "fileInternal", "fileImageType", "fileImageSize", "fileNotReceived", "fileNotApproved", "fallbackToSMPProhibited", "inlineFileProhibited", "invalidForward", "invalidChatItemUpdate", "invalidChatItemDelete", "hasCurrentCall", "noCurrentCall", "callContact", "directMessagesProhibited", "agentVersion", "agentNoSubResult", "commandError", "agentCommandError", "invalidFileDescription", "connectionIncognitoChangeProhibited", "connectionUserChangeProhibited", "peerChatVRangeIncompatible", "relayTestError", "internalError", "exception"]
ChatFeature = Literal["timedMessages", "fullDelete", "reactions", "voice", "files", "calls", "sessions"]
@@ -1441,6 +1452,7 @@ class ContactShortLinkData(TypedDict):
profile: "Profile"
message: NotRequired["MsgContent"]
business: bool
+ localBadge: NotRequired["LocalBadge"]
ContactStatus = Literal["active", "deleted", "deletedByUser"]
@@ -1687,6 +1699,10 @@ class Format_simplexLink(TypedDict):
simplexUri: str
smpHosts: list[str] # non-empty
+class Format_simplexName(TypedDict):
+ type: Literal["simplexName"]
+ nameInfo: "SimplexNameInfo"
+
class Format_command(TypedDict):
type: Literal["command"]
commandStr: str
@@ -1712,13 +1728,14 @@ Format = (
| Format_uri
| Format_hyperLink
| Format_simplexLink
+ | Format_simplexName
| Format_command
| Format_mention
| Format_email
| Format_phone
)
-Format_Tag = Literal["bold", "italic", "strikeThrough", "snippet", "secret", "small", "colored", "uri", "hyperLink", "simplexLink", "command", "mention", "email", "phone"]
+Format_Tag = Literal["bold", "italic", "strikeThrough", "snippet", "secret", "small", "colored", "uri", "hyperLink", "simplexLink", "simplexName", "command", "mention", "email", "phone"]
class FormattedText(TypedDict):
format: NotRequired["Format"]
@@ -1854,6 +1871,10 @@ class GroupLinkPlan_noRelays(TypedDict):
type: Literal["noRelays"]
groupSLinkData_: NotRequired["GroupShortLinkData"]
+class GroupLinkPlan_updateRequired(TypedDict):
+ type: Literal["updateRequired"]
+ groupSLinkData_: NotRequired["GroupShortLinkData"]
+
GroupLinkPlan = (
GroupLinkPlan_ok
| GroupLinkPlan_ownLink
@@ -1861,9 +1882,10 @@ GroupLinkPlan = (
| GroupLinkPlan_connectingProhibit
| GroupLinkPlan_known
| GroupLinkPlan_noRelays
+ | GroupLinkPlan_updateRequired
)
-GroupLinkPlan_Tag = Literal["ok", "ownLink", "connectingConfirmReconnect", "connectingProhibit", "known", "noRelays"]
+GroupLinkPlan_Tag = Literal["ok", "ownLink", "connectingConfirmReconnect", "connectingProhibit", "known", "noRelays", "updateRequired"]
class GroupMember(TypedDict):
groupMemberId: int # int64
@@ -1939,6 +1961,7 @@ class GroupRelay(TypedDict):
userChatRelay: "UserChatRelay"
relayStatus: "RelayStatus"
relayLink: NotRequired[str]
+ relayCap: "RelayCapabilities"
class GroupRootKey_private(TypedDict):
type: Literal["private"]
@@ -2048,6 +2071,10 @@ class LinkPreview(TypedDict):
image: str
content: NotRequired["LinkContent"]
+class LocalBadge(TypedDict):
+ badge: "BadgeInfo"
+ status: "BadgeStatus"
+
class LocalProfile(TypedDict):
profileId: int # int64
displayName: str
@@ -2057,6 +2084,7 @@ class LocalProfile(TypedDict):
contactLink: NotRequired[str]
preferences: NotRequired["Preferences"]
peerType: NotRequired["ChatPeerType"]
+ localBadge: NotRequired["LocalBadge"]
localAlias: str
MemberCriteria = Literal["all"]
@@ -2226,6 +2254,7 @@ class NewUser(TypedDict):
profile: NotRequired["Profile"]
pastTimestamp: bool
userChatRelay: bool
+ clientService: bool
class NoteFolder(TypedDict):
noteFolderId: int # int64
@@ -2307,6 +2336,7 @@ class Profile(TypedDict):
contactLink: NotRequired[str]
preferences: NotRequired["Preferences"]
peerType: NotRequired["ChatPeerType"]
+ badge: NotRequired["BadgeProof"]
class ProxyClientError_protocolError(TypedDict):
type: Literal["protocolError"]
@@ -2346,6 +2376,12 @@ ProxyError = ProxyError_PROTOCOL | ProxyError_BROKER | ProxyError_BASIC_AUTH | P
ProxyError_Tag = Literal["PROTOCOL", "BROKER", "BASIC_AUTH", "NO_SESSION"]
+class PublicGroupAccess(TypedDict):
+ groupWebPage: NotRequired[str]
+ groupDomain: NotRequired[str]
+ domainWebPage: bool
+ allowEmbedding: bool
+
class PublicGroupData(TypedDict):
publicMemberCount: int # int64
@@ -2353,6 +2389,7 @@ class PublicGroupProfile(TypedDict):
groupType: "GroupType"
groupLink: str
publicGroupId: str
+ publicGroupAccess: NotRequired["PublicGroupAccess"]
class RCErrorType_internal(TypedDict):
type: Literal["internal"]
@@ -2621,6 +2658,9 @@ RcvMsgError = RcvMsgError_dropped | RcvMsgError_parseError
RcvMsgError_Tag = Literal["dropped", "parseError"]
+class RelayCapabilities(TypedDict):
+ webDomain: NotRequired[str]
+
class RelayProfile(TypedDict):
displayName: str
fullName: str
@@ -2682,6 +2722,19 @@ class SimplePreference(TypedDict):
SimplexLinkType = Literal["contact", "invitation", "group", "channel", "relay"]
+class SimplexNameDomain(TypedDict):
+ nameTLD: "SimplexTLD"
+ domain: str
+ subDomain: list[str]
+
+class SimplexNameInfo(TypedDict):
+ nameType: "SimplexNameType"
+ nameDomain: "SimplexNameDomain"
+
+SimplexNameType = Literal["publicGroup", "contact"]
+
+SimplexTLD = Literal["simplex", "testing", "web"]
+
SndCIStatusProgress = Literal["partial", "complete"]
class SndConnEvent_switchQueue(TypedDict):
@@ -3363,8 +3416,9 @@ class User(TypedDict):
sendRcptsSmallGroups: bool
autoAcceptMemberContacts: bool
userMemberProfileUpdatedAt: NotRequired[str] # ISO-8601 timestamp
- uiThemes: NotRequired["UIThemeEntityOverrides"]
userChatRelay: bool
+ clientService: bool
+ uiThemes: NotRequired["UIThemeEntityOverrides"]
class UserChatRelay(TypedDict):
chatRelayId: int # int64
@@ -3397,7 +3451,7 @@ class UserContactRequest(TypedDict):
cReqChatVRange: "VersionRange"
localDisplayName: str
profileId: int # int64
- profile: "Profile"
+ profile: "LocalProfile"
createdAt: str # ISO-8601 timestamp
updatedAt: str # ISO-8601 timestamp
xContactId: NotRequired[str]
diff --git a/packages/simplex-chat-python/tests/test_bot_registration.py b/packages/simplex-chat-python/tests/test_bot_registration.py
index f6f245c344..06837bdc07 100644
--- a/packages/simplex-chat-python/tests/test_bot_registration.py
+++ b/packages/simplex-chat-python/tests/test_bot_registration.py
@@ -86,8 +86,63 @@ def test_bot_profile_to_wire_with_commands():
)
cmds = bot._profile_to_wire().get("preferences", {}).get("commands") or []
assert len(cmds) == 2
+ # `params` defaults to None and must be ABSENT from the wire dict
+ # (not present as `null`/`""`) so the Haskell parser sees
+ # `Nothing` and the SimpleX client sends the bare `/keyword` on
+ # tap rather than pasting a trailing-space placeholder.
assert cmds[0] == {"type": "command", "keyword": "ping", "label": "Ping bot"}
assert cmds[1] == {"type": "command", "keyword": "help", "label": "Show help"}
+ assert "params" not in cmds[0]
+ assert "params" not in cmds[1]
+
+
+def test_bot_command_params_emits_on_wire():
+ """When `BotCommand.params` is set, the wire dict carries it as
+ `params: `. The SimpleX client (verified against
+ `CommandsMenuView.kt:153-161` and `CommandsMenuView.swift:117-128`
+ in simplex-chat 6.5) then pastes `/` into the
+ input box on tap, positions the cursor at the end, and lets the
+ user edit before sending. Use this for commands that take a
+ required argument (`/review `)."""
+ bot = Bot(
+ profile=BotProfile(display_name="x"),
+ db=SqliteDb(file_prefix="/tmp/test"),
+ commands=[
+ BotCommand(keyword="review", label="Review PR", params=""),
+ BotCommand(keyword="order", label="Place order", params=""),
+ ],
+ )
+ cmds = bot._profile_to_wire().get("preferences", {}).get("commands") or []
+ assert cmds[0] == {
+ "type": "command",
+ "keyword": "review",
+ "label": "Review PR",
+ "params": "",
+ }
+ assert cmds[1] == {
+ "type": "command",
+ "keyword": "order",
+ "label": "Place order",
+ "params": "",
+ }
+
+
+def test_bot_command_distinguishes_none_from_empty_params():
+ """`params=None` (immediate send) and `params=""` (paste with
+ trailing space) are semantically different on the client side.
+ Verify the wire form preserves the distinction: None → key
+ absent; empty string → key present with empty value."""
+ bot = Bot(
+ profile=BotProfile(display_name="x"),
+ db=SqliteDb(file_prefix="/tmp/test"),
+ commands=[
+ BotCommand(keyword="send", label="Send", params=None),
+ BotCommand(keyword="paste", label="Paste", params=""),
+ ],
+ )
+ cmds = bot._profile_to_wire().get("preferences", {}).get("commands") or []
+ assert "params" not in cmds[0]
+ assert cmds[1].get("params") == ""
def test_client_profile_to_wire_has_no_bot_extras():
diff --git a/packages/simplex-chat-webrtc/src/call.ts b/packages/simplex-chat-webrtc/src/call.ts
index 5f3d2bf332..8441560013 100644
--- a/packages/simplex-chat-webrtc/src/call.ts
+++ b/packages/simplex-chat-webrtc/src/call.ts
@@ -583,6 +583,8 @@ const processCommand = (function () {
case "capabilities":
console.log("starting outgoing call - capabilities")
if (activeCall) endCall()
+ // Stop a preview stream from an earlier pre-connect outgoing call being replaced (activeCall may be null here)
+ stopNotConnectedCall()
let localStream: MediaStream | null = null
try {
@@ -623,7 +625,8 @@ const processCommand = (function () {
if (activeCall) endCall()
// It can be already defined on Android when switching calls (if the previous call was outgoing)
- notConnectedCall = undefined
+ // Stop its preview tracks before clearing, otherwise camera/mic stay live
+ stopNotConnectedCall()
inactiveCallMediaSources.mic = true
inactiveCallMediaSources.camera = command.media == CallMediaType.Video
inactiveCallMediaSourcesChanged(inactiveCallMediaSources)
@@ -1444,6 +1447,14 @@ const processCommand = (function () {
}
}
+ // Call on any path that abandons notConnectedCall, otherwise its preview camera/mic tracks stay live.
+ function stopNotConnectedCall() {
+ if (notConnectedCall) {
+ notConnectedCall.localStream.getTracks().forEach((track) => track.stop())
+ notConnectedCall = undefined
+ }
+ }
+
function resetVideoElements() {
const videos = getVideoElements()
if (!videos) return
diff --git a/packages/simplex-chat-webrtc/src/desktop/ui.ts b/packages/simplex-chat-webrtc/src/desktop/ui.ts
index eac659a17a..862c727bd5 100644
--- a/packages/simplex-chat-webrtc/src/desktop/ui.ts
+++ b/packages/simplex-chat-webrtc/src/desktop/ui.ts
@@ -2,8 +2,8 @@
useWorker = typeof window.Worker !== "undefined"
isDesktop = true
-// Create WebSocket connection.
-const socket = new WebSocket(`ws://${location.host}`)
+// Create WebSocket connection. location.search carries the per-call ?token=... capability required by the server.
+const socket = new WebSocket(`ws://${location.host}${location.search}`)
socket.addEventListener("open", (_event) => {
console.log("Opened socket")
diff --git a/plans/2026-04-29-member-profile-sending-channels.md b/plans/2026-04-29-member-profile-sending-channels.md
index 2ee36b676e..be7d26ecab 100644
--- a/plans/2026-04-29-member-profile-sending-channels.md
+++ b/plans/2026-04-29-member-profile-sending-channels.md
@@ -1,5 +1,16 @@
# Plan: Member Profile Sending in Channels
+## Implementation note (2026-05-18)
+
+The shipped implementation is **monotonic and reuses `member_relations_vector`**, not a new `sent_profile_vector` column:
+
+- The introduction bit lives in `group_members.member_relations_vector` with status `MRIntroduced`. The M20251117 backfill already populates this column for channel rows (relay role is not admin/owner), and `createNewGroupMember` writes `Binary B.empty` for new members.
+- Bits flip 0 → 1 when the relay first announces the member to a recipient via prepended `XGrpMemNew` (or via `XGrpMemIntro` in `introduceInChannel`'s join-time direct path). They are **never cleared**.
+- Profile updates propagate via the sender's own signed `XInfo`, forwarded unchanged by the relay. The relay updates its DB on receipt; subscribers verify with the key obtained from the earlier `XGrpMemNew`. Section 5 below ("Clear vector on profile update") is superseded by this — no clearing happens.
+- The mutually-exclusive two-column delivery-jobs storage (`single_sender_group_member_id` + `sender_group_member_ids`) collapses into a single nullable `sender_group_member_ids BYTEA` column: `[s]` for single-sender jobs, `[s1, s2, ...]` for multi-sender batches, NULL for sender-less jobs (`DJRelayRemoved`).
+
+The plan body below is preserved for historical context.
+
## Context
In channels (relayed groups), subscribers don't know profiles of other subscribers. When subscriber A sends a reaction/message that gets forwarded to subscriber B, B creates an "unknown member" record with a synthesized name. This degrades UX — subscribers see "unknown member" instead of real profiles.
diff --git a/plans/2026-05-08-public-groups-via-relays-overview.md b/plans/2026-05-08-public-groups-via-relays-overview.md
new file mode 100644
index 0000000000..c0114e237d
--- /dev/null
+++ b/plans/2026-05-08-public-groups-via-relays-overview.md
@@ -0,0 +1,45 @@
+# Public groups via relays — plan summary
+
+A third kind of group: relay-mediated like channels, but every member can post like a
+secret group. Resolves the scale ceiling of full-mesh groups without the broadcast-only
+governance of channels. Two orthogonal axes, already in the model:
+
+| `useRelays` | `groupType` | Name |
+|-------------|--------------|--------------|
+| false | (none) | Secret group |
+| true | `GTChannel` | Channel |
+| true | `GTGroup` | Public group | ← new
+| true | `GTUnknown` | refuse | ← older client sees this for `"group"`
+
+`useRelays` is transport; `groupType` is the governance model (broadcast vs
+participatory). The joiner role is a per-group value the owner sets at creation,
+carried on the (owner-signed) channel profile, so every relay derives the same role for
+the same group — not from a relay-side global config and not from `groupType`. The
+blocker is narrow: no path produces `GTGroup` today, and the channel profile carries
+no joiner-role field yet.
+
+## Shape of the work
+
+Backend: wire/version bump, type helpers, create command, owner-configured joiner-role
+field on the channel profile, relay role derivation from that field. Clients (iOS +
+Kotlin mirror): model, audit splitting transport-vs-governance call sites, unified
+create flow with a Channel/Public-group toggle that picks the joiner-role default,
+views, connect-plan messaging.
+
+## Threat model deltas vs. channels
+
+**Relay can fabricate content as any member** (broader than channels, where it could
+only forge as owners). Same deniability property as channels by design; future fix is
+opt-in content signing.
+
+Everything else in the channel threat model carries over unchanged. Out of scope for
+now: member-to-member DMs in relay-mediated groups — deferred, not killed.
+
+## Sequencing & boundary
+
+Hard prerequisite: the member-profile dissemination plan
+(`2026-04-29-member-profile-sending-channels.md`) lands first. Then backend → iOS →
+Kotlin; platforms ship independently; older clients refuse to join. Owner→relay
+role/rejection-rule communication and owner-signature verification on the channel
+profile by relays are not planned here — both apply to channels equally; neither blocks
+Public groups.
diff --git a/plans/2026-05-08-public-groups-via-relays.md b/plans/2026-05-08-public-groups-via-relays.md
new file mode 100644
index 0000000000..702f06fe7f
--- /dev/null
+++ b/plans/2026-05-08-public-groups-via-relays.md
@@ -0,0 +1,378 @@
+# Plan: Public groups via relays
+
+Date: 2026-05-08
+
+## 1. Overview
+
+Channels (shipped) are relay-mediated groups in which the relay forwards
+content from any sender, but subscribers are pinned to `GRObserver` and
+cannot post. Public groups are the second value of the same two-axis design:
+same wire, same transport, members can post. The blocker is narrow — no
+path produces `groupType = GTGroup`, and the relay's joiner-role default
+comes from a global config instead of the owner-signed channel profile.
+Add a `memberRole` field to the profile, plumb it (with `groupType`)
+through the create command, derive the relay's joiner role from it, audit
+clients for sites that conflate transport with governance, ride on the
+approved member-profile dissemination plan. Member-to-member DMs in
+relay-mediated groups are deferred (§10).
+
+## 2. Concept summary: the `useRelays × groupType` matrix
+
+| `useRelays` | `groupType` | Name | Wire shape | UX |
+|-------------|-------------------------|-------------------|-------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
+| `false` | (no `publicGroup`) | **Secret group** | P2P `x.grp.inv` invitations; full mesh between members; JSON array batch. | Today's group: all members can post; profiles known eagerly; admins moderate. |
+| `true` | `GTChannel` | **Channel** | Relay-mediated; subscribers join via channel link; binary signed-batch format; profile carries `memberRole` (default `GRObserver` at creation). | Today's channel: only owners post; subscribers anonymous to each other. |
+| `true` | `GTGroup` | **Public group** | Same wire as channel; profile carries `memberRole` (default `GRMember` at creation); profile dissemination on demand. | New: every member can post; member-to-member DMs prohibited (deferred); member roster grown lazily via on-demand profile send. |
+| `true` | `GTUnknown _` (decode) | (refuse to join) | Channel link from a newer client; older client sees unknown discriminator. | New clients reject with a clear "needs newer version" message; pre-existing channels unaffected. |
+
+Three axes: **transport** = `useRelays` (topology, batch, signatures,
+delivery); **governance model** = `groupType` (profile dissemination,
+member affordances; member DMs prohibited in any relay-mediated group);
+**joiner role** = `memberRole` on the owner-signed profile, set at
+creation, type-keyed default. Today's client sites branch on `useRelays`
+as a proxy for `isChannel` — that's the audit work (§4.2, §5.2).
+
+## 3. Backend changes
+
+### 3.1 Wire format / protocol
+
+New optional `memberRole :: Maybe GroupMemberRole` on `PublicGroupProfile`
+(owner-signed; relays read from cache). New chat-protocol version
+`publicGroupsVersion` signals understanding of `groupType = "group"` and
+`memberRole`. Older peers decode unknown `groupType` as `GTUnknown`
+(lossless tag preservation already exists) and ignore unknown JSON fields
+— §7 covers behavior. Channel-protocol docs gain a paragraph naming
+`groupType` the discriminator and `memberRole` the owner-set joiner role.
+
+### 3.2 Type changes
+
+`PublicGroupProfile` gains `memberRole :: Maybe GroupMemberRole`. New
+single-line helpers in the same module as `useRelays'`: `groupType'` /
+`memberRole'` (accessors); `isPublicGroup'` (`useRelays' && groupType'
+== Just GTGroup`); `defaultMemberRoleFor` (`GTChannel → GRObserver`,
+`GTGroup → GRMember`, `GTUnknown _ → GRObserver` defensive);
+`joinerRoleFor` (canonical resolver — `memberRole'` if present, else
+`defaultMemberRoleFor groupType'`). `requiresSignature` unchanged for
+MVP; opt-in content signing is future-work mitigation per §6.
+
+### 3.3 API / command changes
+
+`APINewPublicGroup` / `/public group` gain `groupType` (default channel)
+and optional `memberRole` (default `defaultMemberRoleFor groupType`); both
+are written onto the constructed profile. The subscriber-side prepare-group
+flow reads `memberRole` from the resolved link with the same fallback. The
+`channelSubscriberRole` config is removed (no callers after §3.4); tests
+that flipped it migrate to Public groups or explicit `memberRole`.
+
+#### 3.3.1 Default group preferences
+
+Public-group defaults equal secret-group defaults — the channel override
+(`support = OFF`) does not apply, since member-to-moderator escalation is
+expected. Parameterize the existing channel-prefs parser by `GroupType`
+(Channel keeps its path; Public group and `GTUnknown` use secret-group).
+`directMessages` stays ON by inheritance but is **dormant** in any relay-
+mediated group (relay doesn't forward `XGrpDirectInv`; clients hide the
+toggle); keeping the wire ON lets a future plan re-enable DMs without a
+profile-shape change.
+
+### 3.4 Message processing
+
+- **Relay joiner-role derivation** (today reads `channelSubscriberRole`):
+ switch to `joinerRoleFor gInfo`. Eliminates cross-relay disparity.
+- **Member-DM defensive refusal** (`xGrpDirectInv`): when `useRelays'`,
+ emit `messageError` and create no contact. Belt-and-suspenders with the
+ §4/§5 client suppression; unreachable today (no forwarding, no P2P).
+- **Legacy `x.grp.inv`**: existing channel rejection covers Public groups.
+- **`unverifiedAllowed`**: unchanged. Tightening becomes possible once
+ the dissemination plan distributes member keys; existing TODO is
+ updated to name that precondition.
+- **Inherited unchanged** (add a test each): `checkSendAsGroup`
+ (role-based), receipts cutoff (count-based), introduce-in-channel +
+ history (`useRelays`-keyed).
+- **`memberAdmission` on relay-mediated join**: hardcoded `GAAccepted`
+ bypasses review/captcha. Generic relay-mediated-groups gap; §8.
+
+### 3.5 Database migrations
+
+No schema migration: `groupType` and `memberRole` ride the existing
+JSON-serialized profile; absent fields resolve via `defaultMemberRoleFor`.
+The dissemination plan's `sent_profile_vector BLOB` migration is a hard
+prerequisite owned by that plan.
+
+### 3.6 Test scenarios
+
+Add Public-group helpers paralleling the channel helpers, plus:
+
+1. Member sends content; all members receive it (no "unknown member" lines).
+2. Multi-author session: no "unknown member" lines anywhere.
+3. Member edit / delete / react forwarded by relay to all members.
+4. Member-DM refused on receive: inject `XGrpDirectInv`, expect `messageError`, no contact created; repeat for Channel.
+5. Role changes propagate through signed forwarding.
+6. Blocked member's subsequent messages not forwarded.
+7. Multi-relay delivery with cross-relay deduplication.
+8. History on join.
+9. `asGroup=true` from a non-owner member rejected with existing error.
+10. Receipts disabled above the 20-member limit.
+11. Older-client refusal on `groupType = "group"` shows needs-newer-version.
+12. Incognito member posting attributes the incognito profile to others.
+13. `memberRole` propagates: explicit `GRAuthor` at creation → joiners get `GRAuthor`; resolved link data carries the value.
+14. `memberRole` defaults: Channel → `GRObserver`; Public group → `GRMember`.
+15. Old-profile fallback: `memberRole = Nothing` → `defaultMemberRoleFor groupType` (`GRObserver` for Channel).
+
+## 4. iOS changes
+
+### 4.1 Model
+
+Add `case group` to `GroupType` (with serializer arms);
+`memberRole: GroupMemberRole?` on `PublicGroupProfile`; `isPublicGroup`,
+`groupType`, `memberRole` accessors on `GroupProfile`/`GroupInfo`. Client
+uses `memberRole` for display only; authoritative resolution stays on
+Haskell.
+
+### 4.2 Audit `useRelays` vs `isChannel` (≈73 sites)
+
+Per-site rule: **transport** (link/relay management, owner-can't-leave-own-
+relay-group, relay-status indicator, incognito flag display, typing-state
+gating, member-DM-affordance suppression) → keep `useRelays`. **Governance**
+(titles, "subscribers" vs "members" framing, "Channel preferences" labels,
+channel-style vs group-style member display) → switch to `isChannel`.
+Roughly 70% flip to `isChannel`. Visually compare Public / Channel / Secret
+after.
+
+### 4.3 Create flow
+
+Unified view with a "Channel / Public group" segmented control above the
+display-name field, defaulting to Channel. The toggle drives the screen
+title, link-step label, success screen, and two API parameters: `groupType`
+and `memberRole` (`.observer` for Channel, `.member` for Public group —
+no separate role picker in MVP). Default `groupPreferences` builder is
+`groupType`-keyed per §3.3.1. The `directMessages` toggle is hidden in
+the create-flow prefs section when `useRelays`. When `groupType = .group`,
+render below the title:
+
+> "In a Public group, every member can post. Messages are delivered through
+> relays you choose, which means a malicious relay could change or
+> fabricate messages from any member. Pick relays you trust."
+
+### 4.4 Strings, views, icons, connect-plan
+
+- **Strings:** ~5–10 keys mirroring channel forms with `_public_group`
+ suffixes (create/add/leave/delete/link/temporarily-unavailable/no-
+ relays), plus `create_public_group_threat_model_note`. Reuse
+ `group_members_*` for "members" framing; channels keep `_subscriber*`.
+- **Compose:** existing role-based gates allow members to post; **suppress
+ the member-tap "send direct message" affordance in any relay-mediated
+ group** (client side of the DM prohibition; receive gate at §3.4).
+- **Views:** `GroupChatInfoView` and the link view branch three ways at
+ §4.2 sites; the link view takes `groupInfo` and derives variant inside.
+ `GroupPreferencesView` hides `directMessages` when `useRelays`.
+- **Icon:** `chatIconName` gains a Public-group arm with a distinct icon
+ (different from channel-antenna and secret-group-people — §8).
+- **Members view:** show the relay-known roster; header "subscribers" for
+ channels, "members" for Public groups. No filtered view in MVP.
+- **Connect-plan:** wording keyed on resolved `groupType` — "ok to
+ subscribe via relays" (channel) vs "ok to join via relays" (Public
+ group). CLI string changes alongside; tests follow.
+
+## 5. Kotlin changes
+
+Mirror of §4 across the Compose surface. Subsections parallel §4 and
+note divergences only.
+
+### 5.1 Model
+
+`GroupType` gains `Group`; `memberRole` / `isPublicGroup` accessors on
+`GroupInfo` / `GroupProfile`.
+
+### 5.2 Audit `useRelays` vs `isChannel` (≈74 sites)
+
+Same transport-vs-governance rule as §4.2; ~70% flip to `isChannel`.
+
+### 5.3 Create flow
+
+Single-view create with Channel / Public-group toggle driving
+`groupType`+`memberRole`; threat-model note below the title;
+`directMessages` toggle hidden under `useRelays`.
+
+### 5.4 Strings, views, icons, ConnectPlan
+
+Strings, views, icons, and ConnectPlan mirror §4.4. **Kotlin-only:**
+chat-list filter chips place Public groups in the "groups" bucket
+(mental model: "things I can post in"), not "channels".
+
+## 6. Threat model: changes from channels
+
+This section assumes the channel threat model
+(`docs/protocol/channels-overview.md` §"Threat model"). Public groups
+inherit every property listed there. One threat is *broader* (channels
+have a narrower form of the same threat). The relay's "can / cannot"
+framing matches the existing doc style; the items below are written so
+they can be folded directly into a future revision of
+`channels-overview.md` once Public groups ship.
+
+### 6.A.1 A relay can fabricate content as any member
+
+Content messages (`XMsgNew`, `XMsgUpdate`, `XMsgDel`, `XMsgReact`,
+`XFileCancel`) are unsigned in both channels and Public groups
+(`Protocol.hs:1221`, `requiresSignature` lists only roster /
+administrative events). In channels this gives a compromised relay
+the ability to fabricate content attributed to owners — already
+documented in `channels-overview.md` §"Threat model" ("Substitute
+unsigned content or selectively drop messages for its subscribers").
+In Public groups, the same property has a **broader blast radius**:
+the relay can fabricate content attributed to *any* member, not just
+to owners.
+
+This matches the channel deniability property by design (see
+`channels-overview.md` §"Signing scope: roster only, content
+optional"): unsigned content is precisely what enables cryptographic
+deniability — no third party can prove a member authored anything.
+The trade-off is that the operator on the delivery path cannot be
+prevented from forging in the same channel.
+
+**A single compromised relay**
+
+*can:*
+
+- Fabricate content messages attributed to any member, not just to
+ owners. Detectable by other members through cross-relay
+ consistency (same TODO as the channel case: difference detection
+ not yet implemented).
+- Modify the text or content of messages in transit and re-attribute
+ the modified message to its original author.
+- Drop content messages selectively — same property as channels.
+
+*cannot:*
+
+- Forge signed administrative events: `XGrpInfo`, `XGrpPrefs`,
+ `XGrpMemRole`, `XGrpMemRestrict`, `XGrpMemDel`, `XGrpDel`,
+ `XGrpLeave`, `XInfo` (`Protocol.hs:1221`). Roster manipulation,
+ profile changes, and member-attributed leave / profile-update
+ events all require valid signatures.
+- Substitute the channel profile or impersonate an owner — the
+ channel's entity ID and owner authorization chain are validated
+ by every recipient against the channel link. The new `memberRole`
+ field is part of the (owner-signed) channel profile, so a
+ compromised relay also cannot fabricate a different joiner role
+ than the owner configured.
+- Alter authoritative state on owner devices.
+
+**Mitigation.** No code change for the MVP. The future-work fix is
+opt-in content signing per the channel roadmap
+(`channels-overview.md` §"Future work" / "Transcript integrity" /
+"Opt-in content signing"). When that ships, owners of Public groups
+will be able to require all content (member or owner) to carry a
+signature; member keys are already disseminated to other members
+via the prior plan (`2026-04-29-member-profile-sending-channels.md`),
+so verification on the recipient side is not a separate effort.
+
+In the meantime, the create-flow help text for "Public group" on
+both platforms (§4.3, §5.3) carries this trade-off framing: "In a
+Public group, the relay forwards messages on behalf of every member.
+A compromised relay could change message text or attribute fabricated
+messages to any member. Use a secret group if you need non-
+repudiable peer-to-peer messaging." This is the same trade-off
+channels make for owner posts; making it explicit at create time
+lets users choose Public-group-via-relay vs secret-group based on
+whether they value scale or content integrity.
+
+### 6.A.2 What is unchanged from channels
+
+Every other property of the channel threat model carries over
+without change. In particular:
+
+- A relay cannot impersonate an owner or substitute the channel
+ profile (signed events, validated entity ID). The configured
+ `memberRole` is part of the signed profile, so the relay cannot
+ unilaterally elevate or demote joiners relative to what the owner
+ specified.
+- A relay cannot determine subscriber / member real identity or
+ network address (inherited from SMP transport).
+- All-relays-compromised-and-colluding cannot forge signed events
+ or alter owner-authoritative state.
+- A passive network observer cannot determine which Public group a
+ member is in, or correlate Public-group activity with other
+ SimpleX activity.
+
+Public-group members get the same participant-privacy guarantees as
+channel subscribers, and Public-group owners get the same key-loss
+risk profile as channel owners (see `channels-overview.md`
+§"Compromise of owner keys" and §"Loss of all owner devices").
+
+**Out of scope for now: member-to-member DMs in relay-mediated
+groups.** In channels, members do not DM each other today. In Public
+groups, this plan prohibits the affordance (client-side and
+defensively on the receive path) and the relay does not forward
+`XGrpDirectInv`. The relay therefore does not see a "member DM
+graph" — that threat (which a forwarded-DM design would have
+introduced) does not exist under this plan. A future plan can
+re-introduce member-to-member DMs and revisit the metadata trade-off
+explicitly; the design space is sketched in §10.
+
+### 6.A.3 Release-notes line
+
+For the Public-groups release notes, include a one-line summary of
+the new property:
+
+> "In a Public group, the relay you choose could in principle alter
+> or fabricate group messages attributed to any member. Pick relays
+> you trust, or use a secret group if you need peer-to-peer message
+> integrity."
+
+## 7. Migration / compatibility
+
+- **Existing channels unaffected.** Pre-upgrade profiles have no
+ `memberRole`; readers fall back to `defaultMemberRoleFor GTChannel =
+ GRObserver`. No data migration.
+- **Older clients** decode `groupType = "group"` as `GTUnknown` and must
+ refuse to join with a "needs newer version" alert; they ignore unknown
+ fields on channel profiles otherwise.
+- **Older relays** forward Public-group traffic but resolve joiner role
+ from their global config — joiners via un-upgraded relays get the legacy
+ role and cannot post. Mitigation: warn the owner at create time if any
+ selected relay's chat version is below `publicGroupsVersion`. Soft
+ warning, not a hard block.
+
+## 8. Open questions
+
+1. **Future member-DM design.** (i) Relay-forwarded `XGrpDirectInv`
+ (simple; leaks DM-graph metadata); (ii) relay-blind rendezvous via
+ per-member queues on the profile (privacy-preserving; new protocol).
+ Either re-derives §6.
+2. **`memberAdmission` on relay-mediated join.** Hardcoded `GAAccepted`
+ bypasses review/captcha; generic relay-mediated-groups gap; defer.
+3. **Distinct icon for Public groups.** Visually different from channel-
+ antenna and secret-group-people metaphors. Pending design review.
+4. **`channelSubscriberRole` removal.** Verify no out-of-tree consumer
+ reads it before deleting.
+5. **`memberRole` on profile edit.** MVP exposes no UI; Haskell accepts
+ the edit but role-rebase of existing members is undefined. Deferred.
+6. **Roster filter in members view.** Paginate/filter for 100K+ members?
+ Generic relay-roster question; defer.
+7. **Connect-plan wording.** "subscribe / join / connect via relays" —
+ pick per `groupType`; update tests when CLI string changes.
+
+## 9. Sequencing
+
+1. **Prerequisite:** member-profile dissemination plan lands first.
+2. **Backend:** types, `memberRole` field, command parameters, profile-
+ based role derivation, removal of `channelSubscriberRole`, defensive
+ `XGrpDirectInv` refusal, tests 1–15.
+3. **iOS** then **Kotlin** (independent of each other; API defaults are
+ backward compatible): model, audit, create flow, strings; then views,
+ icons, ConnectPlan, DM-affordance suppression.
+4. **Older-client refusal, version-bump release notes, channel-docs
+ updates** ship with the backend release.
+
+## 10. Adjacent work (not planned here)
+
+- **Owner→relay communication of rejection rules.** Joiner-role side is
+ fixed here (travels on the signed profile); rejection-rule side
+ (admission/captcha) is still relay-side config. Future plan: carry it
+ on the profile too.
+- **Owner-signature verification on the channel profile by relays.**
+ Affects channels equally; does not gate this plan.
+- **Member-to-member DMs in relay-mediated groups.** Deferred (§1, §8 Q1).
+ A future plan must re-derive §6 — relay-forwarded DMs would re-introduce
+ (sender, target, time) metadata exposure this plan
+ avoids.
diff --git a/plans/2026-05-15-fix-video-preview-snapshot-hang.md b/plans/2026-05-15-fix-video-preview-snapshot-hang.md
new file mode 100644
index 0000000000..4a64d0ca43
--- /dev/null
+++ b/plans/2026-05-15-fix-video-preview-snapshot-hang.md
@@ -0,0 +1,57 @@
+# Desktop: video playback hangs after a preview snapshot stalls
+
+Branch: `nd/fix-video` · final code commit `4c7073bdc` · PR [#6983](https://github.com/simplex-chat/simplex-chat/pull/6983).
+
+## 1. Problem statement
+
+On Desktop with several videos in a chat, clicking the play button on the second (or any subsequent) video does nothing. The first video plays normally; later ones present a play button that responds to the click but never starts playback. No error dialog appears in the UI. `stderr` shows libvlc and libavcodec noise:
+
+```
+[h264 @ 0x...] get_buffer() failed
+[h264 @ 0x...] thread_get_buffer() failed
+[h264 @ 0x...] decode_slice_header error
+[h264 @ 0x...] no frame!
+... main video output error: Failed to grab a snapshot
+```
+
+The bug appeared after PR [#6924](https://github.com/simplex-chat/simplex-chat/pull/6924) (`ab2d03630`), which switched the preview helper player from the shared `vlcFactory` to a dedicated `vlcPreviewFactory` with `--avcodec-hw=none`. Hardware-accelerated decoding had previously masked the underlying fragility. Scope: Desktop only.
+
+## 2. Root cause
+
+Two compounding defects in `VideoPlayer.desktop.kt`, surfaced by `#6924`:
+
+### 2a. Synchronous `snapshots().get()` blocks the shared `playerThread` indefinitely
+
+`getBitmapFromVideo` ran inside `withContext(playerThread.asCoroutineDispatcher())` — the same single-thread executor used by `play()`/`stop()` for playback. Its loop polls vlcj's snapshot API:
+
+```kotlin
+while (snap == null && start + 1500 > System.currentTimeMillis()) {
+ snap = player.snapshots()?.get()
+ delay(50)
+}
+```
+
+The 1500 ms wall-clock guard only fires *between* calls. `player.snapshots()?.get()` is a synchronous JNI call that, when libvlc cannot produce a frame, waits indefinitely. While it blocks, `playerThread` is held: every queued `playerThread.execute { videoPlaying.value = start(...) }` from a subsequent `play()` click sits in the queue and never runs.
+
+This was confirmed by instrumented printlns: after the first video's preview entered the snapshot loop, the second video's `play()` body executed (UI thread println fires), but its lambda submitted to `playerThread.execute` produced no `lambda started` print — because `playerThread` was stuck inside the JNI call.
+
+### 2b. Helper-player pool reuse exhausts the software h264 buffer pool
+
+`getOrCreateHelperPlayer()` returns a `CallbackMediaPlayerComponent` from `helperPlayersPool`, recycling it across preview generations. With `vlcFactory` (hardware-accelerated by default), this was harmless — the GPU buffer pool was large with different lifecycle semantics. After `#6924` switched the helper to `vlcPreviewFactory` (`--avcodec-hw=none`), libavcodec frames from the previous run were not released cleanly across `stop` + `startPaused`, and the second decoder ran out of buffers (`get_buffer() failed`). The vout never produced a frame, which is the trigger for the hang in 2a.
+
+## 3. Solution summary
+
+`apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt` — single file, +8 / −6 lines. Helper-player pool is preserved as-is.
+
+1. **Replace the polling `snapshots().get()` loop with a `CallbackVideoSurface` capture wrapped in `withTimeoutOrNull`.** The existing `SkiaBitmapVideoSurface` (already used for full-screen playback rendering) is attached to the helper player before `media().startPaused(...)`. Its `RenderCallback.display()` runs as soon as libvlc decodes the first frame, populating `surface.bitmap`. `getBitmapFromVideo` polls `surface.bitmap.value` from inside `withTimeoutOrNull(1500L) { ... }`; the wait is now structurally bounded — the synchronous JNI call is gone. Frame is converted to `BufferedImage` via `ImageBitmap.toAwtImage()` for the existing orientation-correction code path. This addresses 2a directly: a helper that fails to decode (2b) no longer holds the dispatcher.
+
+2. **Move preview generation to a dedicated executor.** A new `previewThread = Executors.newSingleThreadExecutor()` runs `getBitmapFromVideo`. Defense in depth: even if 1500 ms of preview work overlaps with a play click, playback's `playerThread` is free to service it.
+
+The pool is intentionally not touched. Removing it loses the factory-warmup amortization across distinct video URIs without addressing the actual hang (which is in the synchronous snapshot API, not in player reuse).
+
+## 4. Alternatives considered (and rejected)
+
+- **Drop the helper-player pool (initial attempt, commit `4a964c661`).** Replaces every preview's helper with a fresh `CallbackMediaPlayerComponent`. Fixes the symptom by sidestepping pool reuse, but costs the factory-warmup benefit and does not address the underlying blocking JNI call — a single corrupt video could still hang preview generation indefinitely (just on a fresh helper). Superseded by the surface-capture approach.
+- **Keep the pool, reset the helper between uses.** vlcj has no clean reset API; would require `media().release()` + manual re-attach. More code, fragile, doesn't address 2a.
+- **Wrap `snapshots().get()` in a coroutine timeout on a separate IO thread.** `withTimeoutOrNull` cannot cancel a blocked JNI call; the IO thread leaks until libvlc returns (which may be never).
+- **Revert PR #6924.** Restores the masking effect of hardware-accelerated decoding but reintroduces whatever the PR was guarding against, and leaves both 2a and 2b in place.
diff --git a/plans/2026-05-16-desktop-updater-fixes.md b/plans/2026-05-16-desktop-updater-fixes.md
new file mode 100644
index 0000000000..40dbefd11d
--- /dev/null
+++ b/plans/2026-05-16-desktop-updater-fixes.md
@@ -0,0 +1,100 @@
+# Desktop In-App Updater Fixes
+
+## Problem Statement
+
+The desktop in-app updater (`apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt`) silently or visibly fails on three of the four supported desktop platforms:
+
+1. **Windows**: no update dialog ever appears for any Windows user, regardless of how out-of-date the running version is.
+2. **AppImage (x86_64)**: the update dialog appears and the download succeeds, but clicking "Install update" opens the new AppImage in an archive viewer ("Archive format not recognized") instead of installing it. The running AppImage is never replaced.
+3. **AppImage (aarch64)**: no update dialog ever appears for any aarch64 AppImage user.
+
+The desktop installer flow on macOS and the `.deb` flow on Debian-derivative Linux are not affected and remain unchanged.
+
+## Root Causes
+
+### 1. Windows — `which dpkg` IOException swallowed
+
+`chooseGitHubReleaseAssets` (AppUpdater.kt) invokes `Runtime.getRuntime().exec("which dpkg")` unconditionally to detect Debian-derivative systems. On Windows there is no `which.exe`; `CreateProcess` returns error 2 and the JVM throws `IOException: Cannot run program "which"` synchronously from `Runtime.exec`. The exception propagates up through `chooseGitHubReleaseAssets` into `checkForUpdate`'s outer `try { ... } catch (e: Exception) { Log.e(...) }`, which logs to stderr and returns. The user-facing alert is never built.
+
+The `.deb` probe was correct in intent but executed too eagerly: it has no business running on a non-Linux platform.
+
+### 2. AppImage — `xdg-open` is the wrong operation
+
+The Linux branch of `installAppUpdate` calls `xdg-open `. An AppImage is not "installable" in the package-manager sense — it is a self-contained executable that lives at the path stored in the `$APPIMAGE` environment variable (set by the AppImage runtime). On most desktop environments, `xdg-open` resolves the `.AppImage` MIME type to an archive handler (file-roller, ark, engrampa). The handler attempts to read the AppImage as a `.iso`/squashfs archive and fails with "Archive format not recognized". Even when it succeeds, it does not replace the running AppImage — the next launch still runs the old binary.
+
+The existing code had no awareness of `$APPIMAGE` at install time. The `GitHubAsset.isAppImage` field hints at an earlier abandoned attempt at AppImage-specific handling.
+
+### 3. aarch64 AppImage — leading space in asset name
+
+`Platform.desktop.kt` declares:
+
+```kotlin
+LINUX_AARCH64("so", unixConfigPath, unixDataPath, " simplex-desktop-aarch64.AppImage"),
+```
+
+The `githubAssetName` literal has a leading space character. The actual release asset published by `.github/workflows/build.yml` is `simplex-desktop-aarch64.AppImage` (no space — verified against the live GitHub releases API). The exact-name filter in `chooseGitHubReleaseAssets` (`release.assets.filter { it.name == desktopPlatform.githubAssetName }`) never matches, the asset list is empty, and `checkForUpdate` returns at the "No assets to download for current system" branch without ever showing a dialog. Same silent-failure pattern as the Windows bug, single arch in blast radius.
+
+## Solution Summary
+
+Three small, independent commits — one per root cause. None of them changes shared logic; each touches one line (Windows, aarch64) or one branch of the install dispatch (AppImage).
+
+### Fix 1 — Gate the `dpkg` probe on Linux
+
+```kotlin
+// AppUpdater.kt: chooseGitHubReleaseAssets
+} else if (desktopPlatform.isLinux() && !isRunningFromAppImage()
+ && Runtime.getRuntime().exec("which dpkg").onExit().join().exitValue() == 0) {
+```
+
+Single conjunct (`desktopPlatform.isLinux() &&`) added at the start of the `else if`. Boolean short-circuit ensures `Runtime.exec` is never reached on non-Linux. The added gate matches the actual semantic intent: `.deb` is a Linux-only package format. Both the Windows IOException and the (theoretical) macOS misbehavior of the `which` probe are eliminated.
+
+### Fix 2 — AppImage-aware install path
+
+In `installAppUpdate`'s Linux branch, read `System.getenv("APPIMAGE")`:
+
+- If non-null, the running app is an AppImage at that path. Replacing it crash-safely takes two steps, because an atomic file replacement is only possible *within a single filesystem* (POSIX `rename(2)`), and the download lives in the temp dir — usually a different filesystem (tmpfs) from where `$APPIMAGE` lives:
+ 1. `Files.copy` the downloaded file to a staging file (`..update`) in the target's *own* directory. This is the unavoidable cross-filesystem transfer; it is not atomic, but it writes only a sidecar, never the live `$APPIMAGE`.
+ 2. Mark the staging file executable, then `Files.move` it onto `$APPIMAGE` with `ATOMIC_MOVE`. Because staging now shares the target's filesystem, this is a real atomic `rename(2)`: the live file flips from old to new in one indivisible step, never partially written.
+
+ A direct `Files.move(downloaded, target, REPLACE_EXISTING)` is **not** sufficient — across filesystems it copies bytes straight onto the live `$APPIMAGE`, which is neither atomic nor crash-safe (an interrupted copy destroys the user's installed app). `ATOMIC_MOVE` on a cross-filesystem move throws `AtomicMoveNotSupportedException`. Staging on the target's filesystem first is what makes the atomic move possible. On Linux the kernel keeps the running process's open file descriptors valid across the rename: the running app continues to function until the user restarts, at which point the new binary is used.
+- If null, fall back to the existing `xdg-open` path (used for `.deb` install on Debian, which is the only remaining caller of this path after Fix 2).
+
+On any exception (permission denied if the AppImage lives in `/opt/`, target read-only, etc.) the catch deletes the staging file and falls back to `desktopOpenDir(file.parentFile)` — the same fallback the original `xdg-open` path used.
+
+### Fix 3 — Remove leading space from `LINUX_AARCH64` asset name
+
+```kotlin
+LINUX_AARCH64("so", unixConfigPath, unixDataPath, "simplex-desktop-aarch64.AppImage"),
+```
+
+Single character removed. The asset name now matches what `make-appimage-linux.sh` produces and what GitHub releases publish.
+
+## Why three commits, not one
+
+Each fix has a different blast radius, a different fix size, and (potentially) a different review path. Three focused commits let a reviewer judge each one in isolation:
+
+- Windows fix: 1 line, gates a side-effecting `Runtime.exec` on a platform check that the surrounding code already establishes.
+- AppImage install: ~35 lines, introduces new file-system operations (`Files.copy` to a staging file, then `Files.move` with `ATOMIC_MOVE`).
+- aarch64 fix: 1 character, fixes a typo in a string literal.
+
+Bundling them as a single commit would force a reviewer to verify all three at once and would obscure `git blame` on the AppImage install logic, which is the only one of the three that introduces meaningful new behavior.
+
+## Out of scope
+
+The following were identified during the audit (`apps/multiplatform/app-updater-audit.md`) but deliberately deferred to keep this PR focused:
+
+- `msiexec /i ${file.absolutePath}` uses the single-string `Runtime.exec` overload that tokenizes on whitespace; paths containing spaces (uncommon on Windows but possible) break the install.
+- Download failures (network, TLS, disk-full, GitHub error) are caught but only logged; the user sees nothing.
+- `process.children().count() > 0` in the Linux `xdg-open` path is racy and arguably wrong.
+- No SHA256 / signature verification on the downloaded artifact — the updater installs whatever GitHub serves.
+- 24h delay with no retry / backoff on transient network errors.
+- macOS install hardcodes `/Applications/SimpleX.app`.
+
+Each is documented with `file:line` references in the audit; none affects the three platforms this PR fixes.
+
+## Test plan
+
+- **Windows**: built x86_64 MSI via the fork CI workflow [`build-windows-msi.yml`](https://github.com/Narasimha-sc/simplex-chat/actions/runs/25958413517), installed in a Windows VM as version 6.5.1 (intentionally lowered to trigger the check against current stable 6.5.2). Settings → Check for updates → Stable: dialog appeared as expected.
+- **AppImage x86_64**: built locally (host build, GHC 9.6.3, gradle createDistributable, appimagetool), installed and ran on Linux. Settings → Check for updates → Stable: dialog appeared, Download landed file at `/tmp/simplex/simplex-desktop-x86_64.AppImage`, Install replaced `$APPIMAGE` in place. Verified by hashing `$APPIMAGE` before and after.
+- **aarch64 AppImage**: not separately tested. Fix is a 1-character literal change verified against the live GitHub releases API (`simplex-desktop-aarch64.AppImage`, no leading space).
+- **macOS**: no changes to the macOS install branch.
diff --git a/plans/2026-05-20-fix-copy-non-msg-items.md b/plans/2026-05-20-fix-copy-non-msg-items.md
new file mode 100644
index 0000000000..4f0c554357
--- /dev/null
+++ b/plans/2026-05-20-fix-copy-non-msg-items.md
@@ -0,0 +1,50 @@
+# Desktop: text selection copies non-message event items
+
+Branch: `nd/fix-copy-non-msg-items` · code commit `a536452ca` · PR [#6993](https://github.com/simplex-chat/simplex-chat/pull/6993).
+
+## 1. Problem statement
+
+The Desktop "select text in messages" feature (PR [#6725](https://github.com/simplex-chat/simplex-chat/pull/6725)) lets the user drag a selection across several message bubbles and copy it. When the selection spans a chat event/info item — a "connected" event, a member "joined"/"left" event, a call event, an e2ee-info line, a feature-change line — the copied text includes that item's text, even though the item is never shown highlighted as part of the selection.
+
+Expected: only real message text is copied. Observed: event/info text such as "connected" is appended into the clipboard between the selected messages.
+
+### Privacy note
+
+Event/info item text is produced from localized string resources (`generalGetString`, `RcvConnEvent.text`, etc.) — it is rendered in the language the user has chosen for the app, whereas real message text is not. A user who selects and copies a long span of messages carelessly, then pastes it into another chat or app, can therefore leak their chosen interface language through the event lines mixed into the paste. For a privacy-focused messenger this is a metadata leak, not only a cosmetic bug.
+
+## 2. Root cause
+
+`SelectionManager.getSelectedCopiedText` (`TextSelection.kt`) builds the copied string by iterating every merged-item index between the selection bounds and emitting each item's text:
+
+```kotlin
+return (lo..hi).mapNotNull { idx ->
+ val ci = items.getOrNull(idx)?.newest()?.item ?: return@mapNotNull null
+ if (ci.meta.itemDeleted != null && ...) return@mapNotNull null
+ val sel = selectedRange(range, idx) ?: return@mapNotNull null
+ selectedItemCopiedText(ci, sel, linkMode)
+}
+```
+
+For an *interior* item in a multi-item selection, `selectedRange` returns `0 until Int.MAX_VALUE` — the whole item is treated as selected — so its text is emitted unconditionally. The only items previously skipped were deleted ones.
+
+Anchor/focus character tracking (`setupItemSelection`) is wired up only for real message views (`FramedItemView`, `EmojiItemView`); event/info items never register offsets and never compute a highlight range. So an event item caught between two selected messages is invisible to the highlight but fully visible to `getSelectedCopiedText`. The copy logic and the on-screen selection disagreed.
+
+The distinguishing property: a real message has `ci.content.msgContent != null`; every event/info `CIContent` variant returns `msgContent == null`.
+
+## 3. Solution summary
+
+`apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/TextSelection.kt` — one guard, +3 lines.
+
+In `getSelectedCopiedText`, skip any item whose `content.msgContent` is null, alongside the existing deleted-item filter:
+
+```kotlin
+if (ci.content.msgContent == null) return@mapNotNull null
+```
+
+Only real messages now contribute copied text — which is exactly the set of items that are selectable and highlighted, so the clipboard matches the visible selection. `content.msgContent` is the existing model property used elsewhere to tell a real message apart from an event/info item.
+
+## 4. Alternatives considered (and rejected)
+
+- **Special-case only "connected" events.** Matches the literal report but leaves the identical bug for every other event/info item (joined/left, calls, e2ee info, feature changes) — same class, same language leak.
+- **Make event items non-selectable / consume the drag.** Larger change to the selection gesture; event items are already non-anchorable, and the bug is purely in the copy aggregation, not in the gesture.
+- **Filter at the call site (`onCopySelection`).** Duplicates the message/non-message distinction outside the one function that owns copied-text assembly; `getSelectedCopiedText` is the correct single source.
diff --git a/plans/2026-05-20-fix-hold-on-long-msg-android.md b/plans/2026-05-20-fix-hold-on-long-msg-android.md
new file mode 100644
index 0000000000..f89aa26582
--- /dev/null
+++ b/plans/2026-05-20-fix-hold-on-long-msg-android.md
@@ -0,0 +1,68 @@
+# Fix chat item long-press menu and ripple shape
+
+Branch: `nd/fix-hold-on-long-msg-android` · PR [#6997](https://github.com/simplex-chat/simplex-chat/pull/6997) · issue [#6991](https://github.com/simplex-chat/simplex-chat/issues/6991).
+
+## 1. Problem statement
+
+Two issues with the chat-item bubble on the multiplatform UI:
+
+- **Android (#6991):** long-pressing the lower part of a very tall text message did not open the select/copy/reply context menu. Long-press on the top/middle worked. Reproduced with a long multi-line message (~150+ lines — e.g. 5000 random bytes as hex); never reproduced on short messages. Occurs **only with the message tail enabled** (bubble shape); with the tail preference disabled, messages use a plain rounded-rectangle shape and the bug does not reproduce. iOS unaffected.
+- **Desktop:** the chat-item press ripple, in some cases, rendered as a rectangle instead of following the rounded bubble shape.
+
+## 2. Solution summary
+
+One function — `Modifier.clipChatItem` in `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt`. It clipped the chat item with `Modifier.clip(shape)` for every shape style. It now clips the **bubble** (`GenericShape`) in the draw pass with `drawWithCache` + `clipPath`, and keeps `Modifier.clip` for the **`RoundRect`** shape, which is unaffected by the bug (§3).
+
+```kotlin
+return when (style) {
+ is ShapeStyle.Bubble -> {
+ val shape = chatItemShape(cornerRoundness, LocalDensity.current, style.tailVisible, chatItem?.chatDir?.sent == true)
+ this.drawWithCache {
+ val path = Path().apply { addOutline(shape.createOutline(size, layoutDirection, this@drawWithCache)) }
+ onDrawWithContent { clipPath(path) { this@onDrawWithContent.drawContent() } }
+ }
+ }
+ is ShapeStyle.RoundRect -> this.clip(RoundedCornerShape(style.radius * cornerRoundness))
+}
+```
+
+Net diff: 1 file (`ChatItemView.kt`), +20 / −5 — the `clipChatItem` function restructured plus two imports.
+
+## 3. Root cause
+
+`Modifier.clip(shape)` is defined in Compose as `graphicsLayer(shape = shape, clip = true)`. A clipping graphics layer restricts **both** drawing **and** pointer hit-test to the shape.
+
+`clipChatItem` is the first (outermost) modifier on the chat-bubble `Column`, and that same `Column` carries the `combinedClickable` long-press handler. So the layer's hit-test region gates every press on the bubble.
+
+- **Android:** for a very tall chat item the layer's hit-test region does not cover the bubble's lower portion — a press there is never delivered to `combinedClickable`, so the long-press menu does not open. This is specific to the bubble's `GenericShape` clip: with the tail disabled the item is clipped with a `RoundedCornerShape`, which hit-tests correctly. The exact reason the `GenericShape` clip's hit-test falls short on tall content was not isolated; the fix does not depend on it (see §4).
+- **Desktop:** the layer's clip did not always extend to the `combinedClickable` press ripple, so the ripple drew to its own rectangular bounds instead of the bubble shape.
+
+## 4. The fix
+
+For the bubble shape, `clipChatItem` clips with a draw modifier instead of a graphics layer. `drawWithCache` builds the shape's `Path` once per size change; `onDrawWithContent { clipPath(path) { drawContent() } }` wraps the whole content draw — bubble background, text, and the press ripple — in a canvas clip.
+
+A draw modifier affects **only drawing**. It is not a layout or pointer-input node and has no effect on hit-test. Therefore:
+
+- the bubble and ripple are still clipped to the shape — visually identical to `Modifier.clip`;
+- pointer hit-test is no longer clipped — `combinedClickable` receives presses anywhere in the `Column`'s bounds, fixing the Android long-press;
+- the canvas `clipPath` clips the ripple reliably, fixing the rectangular desktop ripple.
+
+The `RoundRect` shape keeps `Modifier.clip`: it hit-tests correctly (no bug) and keeps its antialiased outline clip. Scoping by shape — rather than draw-clipping every shape — leaves every non-bubble chat item (service/event messages, tails-off messages, old Android) byte-for-byte unchanged.
+
+## 5. Alternatives rejected
+
+- **Remove `clipChatItem` from the bubble `Column`.** Fixes the Android long-press, but the press ripple loses its shape and renders as a rectangle. Intermediate state during development; replaced.
+- **Draw-pass clip for every shape, unconditionally.** Also correct and a hair simpler (no `when`), but it needlessly moves the `RoundRect` shape off `Modifier.clip`'s antialiased outline clip onto a canvas `clipPath` — a behaviour change with no benefit, since `RoundRect` has no bug. Scoping to the bubble shape keeps `RoundRect` unchanged.
+- **Keep `Modifier.clip`, move `combinedClickable` off the clipped `Column`.** A larger structural change to the chat-item layout tree; the draw-pass clip fixes both issues without moving anything.
+
+## 6. Verification
+
+- **Android** (debug APK): long-press on the lower half of a 150+-line message opens the context menu; top/middle still work; the tap ripple stays bubble-shaped; swipe-to-reply and link tap/long-press are unaffected.
+- **Desktop** (Linux AppImage): the chat-item press ripple follows the bubble shape (rounded corners and tail), not a rectangle — confirmed against a build without the fix.
+- The bubble draw-pass clip above was verified on those Android and desktop builds; this revision additionally keeps `Modifier.clip` for the `RoundRect` shape, which is the unchanged pre-fix behaviour.
+
+## 7. Risk and rollback
+
+- Blast radius: the `Bubble` branch of `clipChatItem`. The `RoundRect` branch is unchanged (`Modifier.clip` as before), so service/event items, tails-off messages and old-Android items are untouched. For the bubble, drawing is clipped identically; the single behavioural change is that pointer hit-test on the bubble is no longer shape-clipped — benign (bubble corners are transparent; a rectangular hit area is a marginally larger touch target).
+- iOS is a separate codebase and is untouched.
+- Rollback: revert the fix commit on the branch, or drop it before merge.
diff --git a/plans/2026-05-20-member-deletion-fulldelete.md b/plans/2026-05-20-member-deletion-fulldelete.md
new file mode 100644
index 0000000000..9d6e516b00
--- /dev/null
+++ b/plans/2026-05-20-member-deletion-fulldelete.md
@@ -0,0 +1,73 @@
+# Full delete on member removal under fullDelete preference
+
+Plan for the next attempt at the change previously tried in PR #6831 (closed as too messy: the member row was deleted twice on one path, and the user's own membership row was deleted when the user was the one removed). The change is small: two SQL-function edits, one new chat-layer helper, an explicit fullDelete branch plus order swap in two backend handlers, and one in-memory removal branch in `removeMemberItems` on each UI platform.
+
+## Problem
+
+When a member is removed via `XGrpMemDel` with `withMessages = True` and the group's `fullDelete` preference is on for the deleter's role, the member's chat items are currently rewritten to `CIModerated` placeholders by `updateMemberCIsModerated`, and the member row is preserved by `deleteOrUpdateMemberRecord` when any item references it. The intent of `fullDelete` is physical deletion. The current behavior leaves placeholder rows and, because `deleteOrUpdateMemberRecord` runs before the items pass, the relay subpath of the latter deletes the member row first and the subsequent file collection returns nothing — files on disk leak. The same ordering bug exists on the moderator side (`APIRemoveMembers`).
+
+## What changes
+
+In `xGrpMemDel` (`src/Simplex/Chat/Library/Subscriber.hs:3157`), only on the branch where `withMessages = True` AND `groupFeatureMemberAllowed SGFFullDelete m gInfo`:
+
+**Case A — the deleted member is the user themselves (`memId == membership.memberId`).** The user's own sent items (those with `group_member_id IS NULL AND item_sent = 1`) and their files are physically deleted. The `membership` row stays with status `GSMemRemoved`, so the group can still be loaded in the chat list and opened.
+
+**Case B — the deleted member is somebody else.** The member's chat items and their files are physically deleted, then the `group_members` row is deleted. Historical system event items that referenced this member as `item_deleted_by_group_member_id` survive with NULL via the existing `ON DELETE SET NULL`.
+
+The non-fullDelete branch, the `withMessages = False` branch, and the entire message-moderation path (`XMsgDel`, `APIDeleteMemberChatItem`, `deleteGroupCIs`, `markGroupCIsDeleted`, `createCIModeration`, `chat_item_moderations`) are not changed.
+
+## Implementation
+
+The whole change is two SQL-function edits in `Store/Messages.hs`, a new member-record helper in `Library/Internal.hs`, and explicit branching plus an order swap in both `xGrpMemDel` (recipient side) and `APIRemoveMembers` (moderator side).
+
+**Edit 1 — rewrite `updateMemberCIsModerated` to physically delete.** Recommended rename: `deleteMemberCIs`. Keep the existing `memId == groupMemberId' membership` branch unchanged (the membership branch selects `WHERE group_member_id IS NULL AND item_sent = 1`; the other branch selects `WHERE group_member_id = ?`). Change the body from "UPDATE chat_items SET moderated content" to "physically delete chat_items + side-table rows analogous to `deleteGroupChatItem` in bulk": delete from `chat_item_messages`, `chat_item_versions`, `chat_item_reactions`, then `DELETE FROM chat_items`. The function loses the `byGroupMember`, `msgDir`, and `deletedTs` parameters since they were only used to construct the moderated content. The chat-layer wrappers `deleteGroupMemberCIs` and `deleteGroupMembersCIs` follow the same signature simplification.
+
+**Edit 2 — extend `getGroupMemberFileInfo` to handle the membership case.** Today it queries `WHERE group_member_id = ?` only, so for Case A it returns nothing and files for the user's own sent items leak (this is a pre-existing bug on both the off and on paths — `markGroupMemberCIsDeleted_` also relies on this function to cancel in-progress transfers). Add the same `memId == groupMemberId' membership` branch as in `deleteMemberCIs`: for the membership case, query `WHERE group_member_id IS NULL AND item_sent = 1`. The only two callers (`deleteGroupMemberCIs_` and `markGroupMemberCIsDeleted_`) both benefit from the fix.
+
+**Edit 3 — add `fullyDeleteMemberRecord` helper in `Library/Internal.hs` next to `deleteOrUpdateMemberRecord`.** Wraps `deleteSupportChatIfExists` + `deleteGroupMember`, returns updated `GroupInfo`. No `isRelay` branch and no `checkGroupMemberHasItems` query — the caller has already physically deleted the member's items, so the existence check would be a wasted query and the function communicates intent explicitly: unconditional row deletion. The `CM` wrapper plus an `IO` variant (`fullyDeleteMemberRecordIO`) mirror the shape of the existing `deleteOrUpdateMemberRecord` / `deleteOrUpdateMemberRecordIO`.
+
+**Edit 4 — swap order and add explicit branching in `xGrpMemDel` Case B (the `else` branch).** Move `when withMessages $ deleteMessages gInfo'' deletedMember' SMDRcv` to run *before* the member-record decision, on the same `gInfo` (the new `deleteMessages` reads only `groupId` and `membership` from the passed `gInfo`). Replace the current member-record dispatch with an explicit branch:
+
+```
+gInfo' <- case deliveryScope of
+ Just (DJSMemberSupport _) | shouldForward -> updateMemberRecordDeleted user gInfo deletedMember GSMemRemoved
+ _ -> if withMessages && groupFeatureMemberAllowed SGFFullDelete m gInfo
+ then fullyDeleteMemberRecord user gInfo deletedMember
+ else deleteOrUpdateMemberRecord user gInfo deletedMember
+```
+
+`deleteMemberItem` (the RGE event creation) keeps its current position after `updatePublicGroupData`. Case A (the `then` branch) needs no order change — the membership row is never deleted there, and `deleteMessages` already runs in the right relative position.
+
+The `DJSMemberSupport _ | shouldForward` subcase keeps its existing `updateMemberRecordDeleted` call regardless of fullDelete — the row is preserved for support-scope forwarding. Under fullDelete the items are still gone (the `deleteMessages` step ran first), the row stays.
+
+**Edit 5 — mirror the swap and explicit branching in `APIRemoveMembers` (`src/Simplex/Chat/Library/Commands.hs:2834`).** Inside `deleteMemsSend`, compute `fullDelete = withMessages && groupFeatureUserAllowed SGFFullDelete gInfo` once. Move the items pass to before `delMember`: run `deleteMessages user gInfo memsToDelete` inside `deleteMemsSend` before the `withStoreBatch'` that calls `delMember`. Change `delMember` to branch explicitly:
+
+```
+delMember db m = do
+ if fullDelete
+ then void $ fullyDeleteMemberRecordIO db user gInfo m
+ else void $ deleteOrUpdateMemberRecordIO db user gInfo m
+ pure m {memberStatus = GSMemRemoved}
+```
+
+`deletePendingMember` flows through `deleteMemsSend` and inherits the new behavior. The outer line 2864 call (`when withMessages $ deleteMessages user gInfo' deleted`) collapses — items are already handled inside `deleteMemsSend` for current and pending members, and invited members (handled by `deleteInvitedMems`) have no chat items. Remove it.
+
+**Edit 6 — extend `removeMemberItems` on both UI platforms to physically remove items from the in-memory list when `fullDelete.on`.** Today (iOS `apps/ios/Shared/Model/ChatModel.swift:814-846`, Kotlin `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt:699-734`) the function walks the in-memory items, identifies matches by direction and member id, and sets `itemDeleted = .moderated(...)`; under `fullDelete.on` it additionally rewrites content to `Snd/RcvModerated`. Items are never removed from `im.reversedChatItems` / `chatItems.value`. After the backend change, the chat_item rows are physically gone in DB while the UI keeps stale moderated placeholders until the next refetch — flicker. Extend the existing `fullDelete.on` branch so it also removes matching items from the in-memory list (iOS: drop them from `im.reversedChatItems`, decrement unread counters, stop voice playback on dropped items; Kotlin: `removeAllAndNotify { isMemberItem(it) }` equivalent, decrement counters, stop audio). The fullDelete-off branch is unchanged (still marks moderated in place).
+
+The three callers — iOS `removeMember` in `GroupChatInfoView.swift:977`, Kotlin `removeMembers` in `GroupChatInfoView.kt:1316`, Kotlin `removeMember` in `GroupMemberInfoView.kt:339`, and the event handlers for `.deletedMember`/`.deletedMemberUser` in `SimpleXAPI.swift:2578-2596` and `SimpleXAPI.kt:2945-2973` — all converge on the same `removeMemberItems` function on each platform and inherit the new behavior automatically. The chat-list preview path inside `removeMemberItems` (the `else` branch that updates `chat.chatItems[0]`) also needs to drop the preview item under fullDelete so the chat list doesn't show a stale moderated last-message.
+
+The `fullDelete.on` gate matches the backend's `groupFeatureMemberAllowed SGFFullDelete` / `groupFeatureUserAllowed SGFFullDelete` because FullDelete is a `GroupFeatureNoRoleI` feature — the role check collapses to the `.on` check.
+
+## Anti-patterns from PR #6831 to avoid
+
+No path may call `deleteGroupMember` twice. No path under Case A may delete the `membership` row — that row must survive. File info must be collected before any chat-item deletion, since `getGroupMemberFileInfo` reads `chat_items`. Do not rely on `ON DELETE SET NULL` to clean up the deleted member's authored items — they are deleted explicitly first. `fullyDeleteMemberRecord` is the only function that should call `deleteGroupMember` directly on the new path; do not duplicate that call in the handler.
+
+## Tests
+
+Add cases in `tests/ChatTests/Groups.hs` for: Case A (user removed by admin, fullDelete on — user's sent items and their files gone, `membership` row exists with `GSMemRemoved`, group still loadable); Case B (member removed by admin, fullDelete on — member's items and files gone, `group_members` row gone, system event items previously referencing the removed member now have NULL `item_deleted_by_group_member_id` and still display correctly); regression for fullDelete=off (items become `CIModerated` placeholders via `markMemberCIsDeleted`); regression for `withMessages = False` (items untouched, row handled by existing path); regression that message moderation under fullDelete=on still produces `CIModerated` placeholders, confirming the moderation path is unchanged. Verify the same Case A and Case B behaviors over both XGrpMemDel (recipient side, Subscriber.hs) and APIRemoveMembers (moderator side, Commands.hs).
+
+UI checks for the manual smoke test: in a group with fullDelete on, remove a member with messages — that member's bubbles disappear immediately from the open chat view on both moderator's and recipients' devices, the chat list preview updates to the previous non-deleted message, and the unread/report counters decrement; with fullDelete off, the same removal produces moderated placeholders as today. Verify on iOS, Android, and Desktop.
+
+## Open items for review
+
+Naming of the rewritten `updateMemberCIsModerated`: `deleteMemberCIs` is the natural rename (the function physically deletes chat items associated with a member, handling the membership case internally). Naming of the new chat-layer helper: `fullyDeleteMemberRecord` (parallels `deleteOrUpdateMemberRecord`). Confirm or amend before implementation.
diff --git a/plans/2026-05-25-channel-web-preview.md b/plans/2026-05-25-channel-web-preview.md
new file mode 100644
index 0000000000..561d3948e7
--- /dev/null
+++ b/plans/2026-05-25-channel-web-preview.md
@@ -0,0 +1,596 @@
+# Channel Web Preview
+
+## Context
+
+SimpleX channels are public - anybody with the link to join and chat relays rebroadcasting the messages can see content. To grow channels, owners need a public web preview (like Telegram's `t.me/s/channelname`) showing the last 50 messages. This lets potential subscribers browse before joining.
+
+The relay already stores all messages in its database. The web preview is a periodic read-and-render loop that writes JSON files served by Caddy, with CORS controlling which domains can embed the preview.
+
+This feature integrates with the `.simplex` namespace (ENS-based names resolving to channel links). A channel's registered domain (`groupDomain`) lives in `PublicGroupAccess` inside `PublicGroupProfile` and is disseminated with the profile. On-chain verification of the domain is deferred until RSLV resolution protocol ships.
+
+## Architecture
+
+```
+simplex-chat CLI (--relay --web-json-dir=... --web-base-url=...)
+ ├── Main chat loop (existing)
+ ├── Relay logic (existing, gated by --relay)
+ └── Web preview thread (new, gated by relayWebOptions)
+ ├── Periodic: load publishable groups → render JSON → write files
+ └── Regenerate Caddy CORS config → caddy reload
+
+Caddy (operator-configured)
+ ├── Serves JSON at https:///group/.json
+ └── Imports generated CORS config file
+
+Channel page (static HTML+JS, hosted by owner or on GitHub)
+ ├── Fetches JSON from relay(s) with fallback
+ └── Renders messages, shows join button
+```
+
+## Data Model Changes
+
+### 1. Extend `PublicGroupProfile` with domain and web access settings
+
+**File:** `src/Simplex/Chat/Types.hs` (line 796)
+
+Current:
+```haskell
+data PublicGroupProfile = PublicGroupProfile
+ { groupType :: GroupType,
+ groupLink :: ShortLinkContact,
+ publicGroupId :: B64UrlByteString
+ }
+```
+
+New:
+```haskell
+data PublicGroupAccess = PublicGroupAccess
+ { groupWebPage :: Maybe Text, -- channel's web page URL (adds CORS origin)
+ groupDomain :: Maybe Text, -- domain for this channel (must have link set in domain record in the contract)
+ domainWebPage :: Bool, -- show on the domain's page (e.g. simplexnetwork.org site for simplex TLD domains, or domain site for web domains)
+ allowEmbeding :: Bool -- allow embedding from any origin (CORS: *)
+ }
+
+data PublicGroupProfile = PublicGroupProfile
+ { groupType :: GroupType,
+ groupLink :: ShortLinkContact,
+ publicGroupId :: B64UrlByteString,
+ publicGroupAccess :: Maybe PublicGroupAccess -- NEW: web preview settings
+ }
+```
+
+`groupDomain` stores the channel's registered `.simplex` domain name or another supported TLD. It is:
+- Set by the owner after registering a name on-chain
+- Disseminated to all members via `GroupProfile` (nested in `publicGroup`)
+- Used by `simplexnetwork.org/c/` to route to the channel's web preview (for .simplex domain)
+
+JSON instances: TH-derived `$(JQ.deriveJSON defaultJSON ''PublicGroupAccess)`. Existing `$(JQ.deriveJSON defaultJSON ''PublicGroupProfile)` covers the new optional field.
+
+**Migration (SQLite/Postgres):** separate columns, same pattern as `group_type`/`group_link`/`public_group_id`:
+```sql
+ALTER TABLE group_profiles ADD COLUMN group_web_page TEXT;
+ALTER TABLE group_profiles ADD COLUMN group_domain TEXT;
+ALTER TABLE group_profiles ADD COLUMN domain_web_page INTEGER;
+ALTER TABLE group_profiles ADD COLUMN allow_embedding INTEGER;
+ALTER TABLE group_profiles ADD COLUMN group_domain_verified_at TEXT;
+```
+
+`group_domain_verified_at` is relay-local verification state (nullable timestamp, NULL = unverified).
+
+**Store changes:**
+
+`src/Simplex/Chat/Store/Shared.hs` line 693 - new constructor alongside `toPublicGroupProfile`:
+```haskell
+toPublicGroupAccess :: Maybe Text -> Maybe Text -> Maybe BoolInt -> Maybe BoolInt -> Maybe PublicGroupAccess
+toPublicGroupAccess groupWebPage groupDomain domainWebPage_ allowEmbeding_
+ | isJust groupWebPage || isJust groupDomain || fromBI domainWebPage_ || fromBI allowEmbeding_ =
+ Just PublicGroupAccess {groupWebPage, groupDomain, domainWebPage = fromBI domainWebPage_, allowEmbeding = fromBI allowEmbeding_}
+ | otherwise = Nothing
+ where fromBI = maybe False unBI
+```
+
+Extend `toPublicGroupProfile` to accept and pass through `Maybe PublicGroupAccess`.
+
+`GroupInfoRow` type (line 668) gains columns for: `group_web_page`, `group_domain`, `domain_web_page`, `allow_embedding`, `group_domain_verified_at`.
+
+`src/Simplex/Chat/Store/Groups.hs`:
+- INSERT (line 367): add all new columns
+- SELECT (line 2375): add `gp.group_web_page`, `gp.group_domain`, `gp.domain_web_page`, `gp.allow_embedding`, `gp.group_domain_verified_at`
+- UPDATE (line 1922): include new columns in `updateGroupProfile_`
+
+### 2. `RelayCapabilities` record, extend `XGrpRelayAcpt`, new `XGrpRelayCap`
+
+**File:** `src/Simplex/Chat/Protocol.hs`
+
+New record for relay capabilities (extensible for future fields):
+```haskell
+data RelayCapabilities = RelayCapabilities
+ { webDomain :: Maybe Text
+ }
+```
+
+TH-derived JSON. All fields optional so old relays produce `{}` and new fields are backward compatible.
+
+**`XGrpRelayAcpt`** - carries capabilities at acceptance time:
+
+Current (line 444): `XGrpRelayAcpt :: ShortLinkContact -> ChatMsgEvent 'Json`
+New: `XGrpRelayAcpt :: ShortLinkContact -> RelayCapabilities -> ChatMsgEvent 'Json`
+
+Parsing: `XGrpRelayAcpt_ -> XGrpRelayAcpt <$> p "relayLink" <*> (p "relayCap" <|> pure defaultRelayCap)`
+Encoding: `XGrpRelayAcpt relayLink cap -> o ["relayLink" .= relayLink, "relayCap" .= cap]`
+Backward compatible: old relays omit `relayCap`, parsed as default (all `Nothing`).
+
+**`XGrpRelayCap`** - new message for ongoing capability updates:
+
+```haskell
+XGrpRelayCap :: RelayCapabilities -> ChatMsgEvent 'Json
+```
+
+Tag: `"x.grp.relay.cap"`
+Parsing: `XGrpRelayCap_ -> XGrpRelayCap <$> p "relayCap"`
+Encoding: `XGrpRelayCap cap -> o ["relayCap" .= cap]`
+
+Sent by relay to owner only when capabilities change (not periodic). Relay detects change by comparing current config against persisted state on startup.
+
+### 3. Store `webDomain` per relay
+
+**File:** `src/Simplex/Chat/Operators.hs` (line 278)
+
+Current:
+```haskell
+data GroupRelay = GroupRelay
+ { groupRelayId :: Int64,
+ groupMemberId :: Int64,
+ userChatRelay :: UserChatRelay,
+ relayStatus :: RelayStatus,
+ relayLink :: Maybe ShortLinkContact
+ }
+```
+
+Add: `relayCap :: Maybe RelayCapabilities`
+
+Stored as separate columns (same pattern as `PublicGroupAccess`):
+**Migration:** `ALTER TABLE group_relays ADD COLUMN base_web_url TEXT`
+
+`relayCap` constructed from columns: `Just RelayCapabilities {webDomain}` when any capability column is non-NULL, `Nothing` otherwise.
+
+**Handlers in `src/Simplex/Chat/Library/Subscriber.hs`:**
+- `XGrpRelayAcpt` (line 770): store `RelayCapabilities` in relay record on acceptance
+- `XGrpRelayCap` (new handler): update `RelayCapabilities` in relay record; only accepted from relay members (`isRelay m`), owner receives
+
+**Relay-side persistence:** relay persists its current `RelayCapabilities` (derived from `RelayWebOptions`) so it can detect config changes on restart. On startup, if persisted capabilities differ from config, relay sends `XGrpRelayCap` to all group owners it serves.
+
+### 4. CLI options for web preview
+
+**File:** `src/Simplex/Chat/Options.hs`
+
+New record bundling all web preview options:
+```haskell
+data RelayWebOptions = RelayWebOptions
+ { webJsonDir :: FilePath, -- --web-json-dir: where to write JSON files
+ webDomain :: Text, -- --web-base-url: public URL prefix (sent in XGrpRelayAcpt)
+ webCorsFile :: FilePath, -- --web-cors-file: generated Caddy CORS config path
+ webUpdateInterval :: Int -- --web-update-interval: seconds (default 300)
+ }
+```
+
+Add as a proper field in `CoreChatOpts`:
+```haskell
+data CoreChatOpts = CoreChatOpts
+ { ...existing...,
+ relayWebOptions :: Maybe RelayWebOptions
+ }
+```
+
+Parsed from CLI: when `--web-json-dir` is provided, all other `--web-*` flags are required. `Nothing` when no web preview flags are set. Only meaningful when `--relay` is also set.
+
+### 5. Web preview thread startup
+
+**File:** `src/Simplex/Chat/Core.hs` (line 74)
+
+Current:
+```haskell
+runSimplexChat ... = do
+ a1 <- runReaderT (startChatController True True) cc
+ when (chatRelay && not testView) $ askCreateRelayAddress cc u
+ forM_ (postStartHook chatHooks) ($ cc)
+ a2 <- async $ chat u cc
+ waitEither_ a1 a2
+```
+
+Add web preview thread as a third async when config is present:
+```haskell
+runSimplexChat ... = do
+ a1 <- runReaderT (startChatController True True) cc
+ when (chatRelay && not testView) $ askCreateRelayAddress cc u
+ forM_ (postStartHook chatHooks) ($ cc)
+ a2 <- async $ chat u cc
+ case relayWebOptions coreOptions of
+ Nothing -> waitEither_ a1 a2
+ Just webOpts -> do
+ a3 <- async $ webPreviewThread webOpts cc
+ void $ waitAnyCancel [a1, a2, a3]
+```
+
+## New Types for JSON Serialization
+
+**File:** new module `src/Simplex/Chat/Web/Preview.hs`
+
+### Reuse as-is (existing ToJSON instances)
+
+- `GroupProfile` (Types.hs:803) - channel metadata (displayName, fullName, shortDescr, description, image, publicGroup incl. groupDomain)
+- `MsgContent` (Protocol.hs:689) - tagged union: MCText, MCLink, MCImage, MCVideo, etc.
+- `LinkPreview` (Protocol.hs:256) - `{uri, title, description, image, content}`
+- `FormattedText` / `MarkdownList` (Markdown.hs:133/139) - parsed markdown
+- `QuotedMsg` / `MsgRef` (Protocol.hs:589) - quoted message context
+- `MsgMentions` = `Map MemberName CIMention` (Messages.hs:264)
+- `CIMention` (Messages.hs:272) - `{memberId, memberRef}`
+- `CIReactionCount` (Messages.hs:338) - `{reaction, userReacted, totalReacted}`
+
+### New types
+
+```haskell
+data WebFileInfo = WebFileInfo
+ { fileName :: String,
+ fileSize :: Integer
+ }
+
+data WebMemberProfile = WebMemberProfile
+ { memberId :: MemberId,
+ displayName :: Text,
+ image :: Maybe ImageData
+ }
+
+data WebMessage = WebMessage
+ { sender :: Maybe MemberId, -- Nothing for CIChannelRcv (forwarded-from-channel)
+ ts :: UTCTime,
+ content :: MsgContent,
+ formattedText :: Maybe MarkdownList,
+ file :: Maybe WebFileInfo,
+ quote :: Maybe QuotedMsg,
+ mentions :: Map MemberName CIMention,
+ reactions :: [CIReactionCount],
+ forwarded :: Maybe CIForwardedFrom,
+ edited :: Bool
+ }
+
+data WebChannelPreview = WebChannelPreview
+ { channel :: GroupProfile, -- NOTE: render loop strips groupDomain until verified
+ subscriberCount :: Maybe Int,
+ members :: [WebMemberProfile],
+ messages :: [WebMessage],
+ updatedAt :: UTCTime
+ }
+```
+
+TH-derived JSON for `WebFileInfo`, `WebMemberProfile`, `WebMessage`, `WebChannelPreview`.
+
+## Render Loop
+
+**File:** new module `src/Simplex/Chat/Web.hs`
+
+Pattern from directory service's `updateListingsThread_` (Service.hs:185-194).
+
+```haskell
+webPreviewThread :: RelayWebOptions -> ChatController -> IO ()
+webPreviewThread opts cc = forever $ do
+ u_ <- readTVarIO $ currentUser cc
+ forM_ u_ $ \user -> do
+ groups <- getWebPublishGroups cc user
+ corsEntries <- forM groups $ \gInfo -> do
+ renderGroupPreview opts cc user gInfo
+ pure (corsEntry gInfo)
+ writeCorsConfig opts corsEntries
+ threadDelay (webUpdateInterval opts * 1_000_000)
+```
+
+### Loading groups
+
+New store function `getWebPublishGroups`:
+```sql
+SELECT ... FROM groups g
+JOIN group_profiles gp ON g.group_profile_id = gp.group_profile_id
+WHERE gp.group_web_page IS NOT NULL
+ AND g.user_id = ?
+```
+
+Returns `[GroupInfo]`. For each, call `getGroupChat` with `CPLast 50` (Store/Messages.hs:1436) to get chat items.
+
+### Converting CChatItem to WebMessage
+
+For each `CChatItem SMDRcv (ChatItem {chatDir, meta, content, mentions, formattedText, quotedItem, reactions, file})`:
+
+1. **Skip if:**
+ - `itemDeleted meta` is `Just _`
+ - `itemTimed meta` is `Just _`
+ - `content` is not `CIRcvMsgContent mc` (skip `CIRcvGroupEvent`, `CIRcvIntegrityError`, etc.)
+ - `mc` is `MCReport` or `MCUnknown`
+
+2. **Extract sender:**
+ - `CIGroupRcv member` -> `Just (memberId member)`, collect member into profiles array
+ - `CIChannelRcv` -> `Nothing` (channel-forwarded message, no individual sender)
+
+3. **Extract file info:**
+ - `file :: Maybe (CIFile 'MDRcv)` has `fileName :: String`, `fileSize :: Integer`
+ - Strip `fileSource`, `fileStatus`, `fileProtocol` (download metadata irrelevant for web)
+
+4. **Build WebMessage:**
+ ```haskell
+ WebMessage
+ { sender = senderMemberId
+ , ts = itemTs meta
+ , content = mc
+ , formattedText = formattedText
+ , file = (\f -> WebFileInfo (fileName f) (fileSize f)) <$> file
+ , quote = quotedItem -- QuotedMsg reused directly
+ , mentions = mentions
+ , reactions = reactions
+ , forwarded = itemForwarded meta
+ , edited = itemEdited meta
+ }
+ ```
+
+5. **Collect unique senders** into `[WebMemberProfile]` from `GroupMember` records in `CIGroupRcv`.
+
+Also include `CIGroupSnd` items (relay's own sent messages, if any - unlikely but possible for admin announcements).
+
+### Filtering unverified domains
+
+Before serializing, the render loop strips `groupDomain` from the `PublicGroupAccess` included in the profile when not verified:
+
+```haskell
+stripUnverifiedDomain :: Maybe UTCTime -> GroupProfile -> GroupProfile
+stripUnverifiedDomain verifiedAt gp = case verifiedAt of
+ Just _ -> gp -- domain verified, include as-is
+ Nothing -> gp {publicGroup = clearDomain <$> publicGroup gp}
+ where
+ clearDomain pgp = pgp {publicGroupAccess = clearAccess <$> publicGroupAccess pgp}
+ clearAccess acc = acc {groupDomain = ""} -- or strip the access record entirely
+```
+
+The `group_domain_verified_at` timestamp is loaded alongside the group info. Until RSLV ships, this column is always NULL, so all domains are stripped from web export.
+
+`domainWebPage` in CORS config is also gated on verified domain - unverified means no domain-site origin in CORS.
+
+### Writing JSON
+
+- Serialize `WebChannelPreview` to JSON via `Data.Aeson.encode`
+- Write atomically (write to temp, rename) to `/.json`
+- `publicGroupId` from `PublicGroupProfile` (base64url-encoded, existing field)
+
+### Generating Caddy CORS config
+
+Write a single file with Caddy `map` directive:
+
+```caddy
+map {path} {cors_origin} {
+ /.json "https://owner-domain.com"
+ /.json "*"
+ default ""
+}
+header /*.json Access-Control-Allow-Origin {cors_origin}
+header /*.json Access-Control-Allow-Methods "GET, OPTIONS"
+```
+
+CORS origin derivation from `PublicGroupAccess`:
+- `allowEmbeding = True` -> `*`
+- `groupWebPage = Just url` -> extract origin from URL (+ domain site origin if `domainWebPage` and domain verified)
+- `groupWebPage = Nothing, domainWebPage = True` -> domain site origin only (when domain is verified)
+- No web page, no embedding, no domain page -> omit from config
+
+After writing, run `caddy reload` if file content changed (compare hash before/after).
+
+## Namespace Integration
+
+`groupDomain` ships now in the profile (inside `PublicGroupAccess`). What's deferred is on-chain verification (RSLV protocol).
+
+### What ships now
+
+1. **`groupDomain :: Text` in `PublicGroupAccess`** - owner sets the registered domain, disseminated to all members
+2. **`domainWebPage :: Bool` in `PublicGroupAccess`** - flag stored but has no effect until domain is verified
+3. **Relay strips `groupDomain` from web export** - no verification means domain is cleared in JSON, no domain-site CORS origin
+
+### What ships with RSLV
+
+1. **RSLV protocol** - relay queries name servers via SMP proxy to verify domain ownership
+2. **`domainWebPage` becomes functional** - enables domain-site hosting (e.g. `simplexnetwork.org/c/`) for verified domains
+3. **In-app resolution** - `#name` markdown (already parsed by namespace branch) resolves and connects
+
+### Verification flow (relay-side)
+
+When owner updates profile with `groupDomain`:
+
+1. **Trigger:** Relay receives profile update on owner's connection containing `groupDomain` field
+2. **Initiate:** Relay sends `RSLV ` through SMP proxy (async, on the same owner connection context)
+3. **Pending state:** `group_domain_verified_at = NULL` in DB. Web export excludes domain while pending.
+4. **Resolution arrives:** `NAME ` agent event arrives on the owner's connection (continuation bound to the connection that sent the profile update)
+5. **Verify:** Check if `channelLinks` in the NAME response includes this group's `groupLink`
+6. **Store result:** Set `group_domain_verified_at = ` on success, leave NULL on failure
+7. **Effect:** Web render loop includes domain in JSON and enables domain-site CORS only when `group_domain_verified_at IS NOT NULL`
+
+Re-verification: periodic (e.g. daily or on each web update cycle) to catch expired/transferred domains. Clear `group_domain_verified_at` when re-verification fails.
+
+### What the namespace branch already provides
+
+- `SimplexNameInfo {nameType, namespace, domain, subDomain}` in Markdown.hs
+- `SimplexName` variant in `Format` ADT
+- Parser for `#name` / `#name.simplex` / `:name.simplex` syntax
+- Forward-compatibility alerts in Kotlin/Swift UI (shows "requires newer app" until resolution is implemented)
+
+## UI Changes (Kotlin/Swift)
+
+### Kotlin types
+
+**File:** `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt`
+
+```kotlin
+@Serializable
+data class PublicGroupAccess(
+ val groupWebPage: String? = null,
+ val groupDomain: String? = null,
+ val domainWebPage: Boolean = false,
+ val allowEmbeding: Boolean = false
+)
+
+// Extend existing PublicGroupProfile (currently at line 2213):
+@Serializable
+data class PublicGroupProfile(
+ val groupType: GroupType,
+ val groupLink: String,
+ val publicGroupId: String,
+ val publicGroupAccess: PublicGroupAccess? = null // NEW
+)
+
+@Serializable
+data class RelayCapabilities(
+ val webDomain: String? = null
+)
+
+// Extend existing GroupRelay:
+@Serializable
+data class GroupRelay(
+ ...existing fields...,
+ val relayCap: RelayCapabilities? = null // NEW
+)
+```
+
+### Owner: Channel info page
+
+**File:** `GroupChatInfoView.kt` (around line 604-606)
+
+After existing `ChannelLinkButton(manageGroupLink)`:
+```kotlin
+ChannelWebPageButton(openChannelWebPage) // owner only
+```
+
+New nav destination opens `ChannelWebPageView`.
+
+### Owner: Channel web page screen
+
+**File:** new `apps/multiplatform/.../views/chat/group/ChannelWebPageView.kt`
+
+- Text field: web page URL (`groupWebPage`)
+- Text field: domain (`groupDomain`)
+- Toggle: allow embedding (`allowEmbeding`)
+- Toggle: show on domain's page (`domainWebPage`) - stored but inert until RSLV ships
+- Section: embed snippet (read-only, auto-generated from relay `webDomain` values + `publicGroupId`)
+- Save button -> `apiUpdateGroup` with updated `GroupProfile`
+
+### Subscriber: Channel info page
+
+In the top section (around line 607-614), after channel link QR:
+```kotlin
+val webPageUrl = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupWebPage
+if (webPageUrl != null) {
+ WebPageLinkRow(webPageUrl) // clickable, opens browser
+}
+```
+
+## Build Configuration
+
+Web preview code compiles into the main `simplex-chat` library (not conditional). The thread only starts when `relayWebOptions` is set in `CoreChatOpts`. Mobile apps never set this.
+
+No cabal flag needed - the thread startup is gated by `Maybe RelayWebOptions` at runtime (same pattern as `chatRelay` gating relay behavior).
+
+## Caddy Setup (operator documentation)
+
+Main Caddyfile (operator writes once):
+```caddy
+relay.example.com {
+ import /etc/caddy/simplex-cors.conf
+ handle /preview/* {
+ root * /var/lib/simplex/web/preview
+ file_server
+ }
+}
+```
+
+Relay CLI invocation:
+```
+simplex-chat --relay \
+ --web-json-dir /var/lib/simplex/web/preview \
+ --web-base-url https://relay.example.com/preview \
+ --web-cors-file /etc/caddy/simplex-cors.conf \
+ --web-update-interval 300
+```
+
+## Channel Page and Embed Code
+
+### Embed snippet (shown to owner)
+
+The "Channel web page" screen auto-generates this from the channel's relay `webDomain` values and `publicGroupId`. Owner copies it into their page:
+
+```html
+
+
+
+```
+
+Example with real values:
+```html
+
+
+
+```
+
+The script fetches `/a1b2c3d4.json`, renders the preview into the `div`. Tries relays in order, falls back on failure. The owner's domain must match the CORS origin configured by the relay (derived from `groupWebPage`), or `allowEmbeding` must be `True` for `*`.
+
+For iframe embedding (when allowed), the snippet is simpler - just an iframe pointing to the owner's hosted channel page.
+
+### Channel page (static JS)
+
+Separate repo or folder. `channel-preview.js` + minimal CSS:
+- Reads config from `data-` attributes on the container div
+- Fetches JSON from relays with fallback (try first, fall back to second)
+- Renders: channel header (name, avatar, description, subscriber count), message list (text with FormattedText markdown, link previews, file indicators, reactions, quotes)
+- Join button: `simplex://` deep link on mobile, QR code on desktop
+- Reuses directory page's markdown rendering approach
+
+## Files to Create/Modify
+
+### New files
+- `src/Simplex/Chat/Web/Preview.hs` - types: `WebChannelPreview`, `WebMessage`, `WebFileInfo`, `WebMemberProfile`
+- `src/Simplex/Chat/Web.hs` - render loop, JSON writing, Caddy config generation
+- `apps/multiplatform/.../views/chat/group/ChannelWebPageView.kt`
+- `apps/ios/Shared/Views/Chat/Group/ChannelWebPageView.swift`
+- Migration files (SQLite + Postgres): `group_web_page`, `group_domain`, `domain_web_page`, `allow_embedding`, `group_domain_verified_at` in group_profiles; `base_web_url` in group_relays
+- Channel page static site (separate repo/folder)
+
+### Modified files
+- `src/Simplex/Chat/Types.hs` - `PublicGroupAccess` type, extend `PublicGroupProfile` with `publicGroupAccess`
+- `src/Simplex/Chat/Protocol.hs` - `RelayCapabilities` record, extend `XGrpRelayAcpt`, add `XGrpRelayCap`
+- `src/Simplex/Chat/Options.hs` - `RelayWebOptions` record, `relayWebOptions :: Maybe RelayWebOptions` in `CoreChatOpts`
+- `src/Simplex/Chat/Core.hs` - start web preview thread in `runSimplexChat`
+- `src/Simplex/Chat/Operators.hs` - `webDomain` in `GroupRelay`
+- `src/Simplex/Chat/Store/Groups.hs` - read/write `PublicGroupAccess` columns; `getWebPublishGroups`
+- `src/Simplex/Chat/Store/Shared.hs` - `toPublicGroupAccess`, extend `toPublicGroupProfile` and `GroupInfoRow`
+- `src/Simplex/Chat/Library/Subscriber.hs` - handle `RelayCapabilities` in `XGrpRelayAcpt` and `XGrpRelayCap`
+- `apps/multiplatform/.../model/ChatModel.kt` - `PublicGroupAccess`, `RelayCapabilities`, `PublicGroupProfile.publicGroupAccess`, `GroupRelay.relayCap`
+- `apps/multiplatform/.../views/chat/group/GroupChatInfoView.kt` - nav link for web page
+- `simplex-chat.cabal` - add `Simplex.Chat.Web.Preview`, `Simplex.Chat.Web` to exposed-modules
+
+## Implementation Order
+
+1. **Data model** - `PublicGroupAccess` in `PublicGroupProfile`, migrations (separate columns), store functions
+2. **Protocol** - `RelayCapabilities`, extend `XGrpRelayAcpt`, add `XGrpRelayCap`, handlers in Subscriber.hs
+3. **CLI options** - `RelayWebOptions` record, `relayWebOptions` field in `CoreChatOpts`
+4. **Web types** - `WebChannelPreview`, `WebMessage`, etc. in new module
+5. **Render loop** - thread startup in Core.hs, periodic JSON generation, Caddy config
+6. **UI (owner)** - "Channel web page" settings screen
+7. **UI (subscriber)** - web page link in channel info
+8. **Channel page** - static HTML+JS template
+9. **Documentation** - operator setup guide
+
+## Verification
+
+1. **Build**: `cabal build simplex-chat` with new modules compiles
+2. **Unit test**: serialize `WebChannelPreview` with sample data, verify JSON matches expected structure
+3. **Integration test**: create channel with `publicGroupAccess` set, run relay with `--web-json-dir`, verify JSON file appears at correct path with correct content
+4. **CORS test**: verify generated config produces correct `Access-Control-Allow-Origin` for configured domains
+5. **UI test**: owner can set web page URL and domain, see embed snippet; subscriber sees clickable link
+6. **Channel page test**: serve static page locally against relay's JSON, verify rendering
+7. **Domain stripping test**: set `groupDomain` on a channel, verify it is stripped from web export JSON (unverified, `group_domain_verified_at IS NULL`)
diff --git a/plans/2026-05-25-fix-e2e-encryption-section-divider.md b/plans/2026-05-25-fix-e2e-encryption-section-divider.md
new file mode 100644
index 0000000000..69d4bd630a
--- /dev/null
+++ b/plans/2026-05-25-fix-e2e-encryption-section-divider.md
@@ -0,0 +1,50 @@
+# Fix E2E encryption section divider rendered inside the section
+
+Branch: `nd/fix-e2e-encryption-section-divider` · base: `master`.
+
+## Problem
+
+On the contact info screen (Android and desktop, current `master`), the "E2E encryption — Quantum resistant / Standard" card has a horizontal divider line cutting across it under its single row, followed by extra padded space — visually reading as the card being sliced in two with a second, empty card underneath. Repros for any 1:1 contact with an active connection. Behaviour of the row itself is correct; bug is purely visual.
+
+## Fix
+
+One line in `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt` — move `SectionDividerSpaced()` out of the `SectionView { ... }` block:
+
+```diff
+ if (conn != null) {
+ SectionView {
+ InfoRow("E2E encryption", if (conn.connPQEnabled) "Quantum resistant" else "Standard")
+- SectionDividerSpaced()
+ }
++ SectionDividerSpaced()
+ }
+```
+
+Total diff: 1 file, +1 / −1.
+
+## Cause
+
+Two unrelated changes combined to produce the visible bug:
+
+1. PR #4060 (`9e3f528d4`, "android: remove experimental PQ toggle") removed the conditional `AllowContactPQButton` / `SectionTextFooter` that used to sit between the `InfoRow` and the divider — but left `SectionDividerSpaced()` inside the `SectionView { ... }` block. At that point `SectionView` was a plain column, so the leftover divider only looked like extra inter-section spacing.
+
+2. PR #6777 (`df5ea3d46`, "android, desktop: new settings section design") wrapped `SectionView`'s content in `CardColumn` with `SectionCardShape`, giving each section a rounded card background. After this, *anything* drawn by the section content — including the leftover `Divider` — is drawn inside the card.
+
+Rendered structure on current master:
+
+```
+SectionView (card background, rounded shape)
+ └ CardColumn
+ ├ InfoRow("E2E encryption", ...)
+ ├ Divider ← line cutting across the card
+ └ 18 dp bottom padding ← reads as a second, empty card
+```
+
+After the fix the divider re-parents to the enclosing `ChatInfoLayout` column and sits between the E2E card and the next section's card, matching the pattern used by every other section on the screen.
+
+## Risk
+
+- One composable call site, structural move of a single node; no logic, state, or styling change.
+- iOS is a separate codebase and is unaffected.
+- Grep confirms no other `SectionDividerSpaced` call sits inside a `SectionView { ... }` in `apps/multiplatform`.
+- Rollback: `git revert` the fix commit.
diff --git a/plans/2026-05-26-fix-video-drag-and-drop.md b/plans/2026-05-26-fix-video-drag-and-drop.md
new file mode 100644
index 0000000000..16258dea7f
--- /dev/null
+++ b/plans/2026-05-26-fix-video-drag-and-drop.md
@@ -0,0 +1,44 @@
+# Fix desktop drag-and-drop of videos attached as files
+
+Branch: `nd/fix-video-drag-and-drop` · base: `master`.
+
+## Problem
+
+On desktop, dragging a video file into a chat attaches it as a generic file (paperclip + filename) instead of as a video (thumbnail + duration). Dragging an image works. Picking the same video via "Gallery → Video" attaches it correctly — so only the drag-and-drop routing is wrong.
+
+## Fix
+
+One file: `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt`. Recognise videos as media in `onFilesAttached`'s classifier.
+
+```diff
+ fun MutableState.onFilesAttached(uris: List) {
+- val groups = uris.groupBy { isImage(it) }
+- val images = groups[true] ?: emptyList()
++ val groups = uris.groupBy { isImage(it) || isVideoUri(it) }
++ val media = groups[true] ?: emptyList()
+ val files = groups[false] ?: emptyList()
+- if (images.isNotEmpty()) {
+- CoroutineScope(Dispatchers.IO).launch { processPickedMedia(images, null) }
++ if (media.isNotEmpty()) {
++ CoroutineScope(Dispatchers.IO).launch { processPickedMedia(media, null) }
+ } else if (files.isNotEmpty()) {
+ processPickedFile(uris.first(), null)
+ }
+ }
++
++private fun isVideoUri(uri: URI): Boolean {
++ val name = getFileName(uri)?.lowercase() ?: return false
++ return name.endsWith(".mov") || name.endsWith(".avi") || name.endsWith(".mp4") ||
++ name.endsWith(".mpg") || name.endsWith(".mpeg") || name.endsWith(".mkv")
++}
+```
+
+Total diff: 1 file, +11 / −5.
+
+## Cause
+
+`onFilesAttached` classified URIs by `isImage` only — non-images (including videos) fell through to `processPickedFile`, producing a `FilePreview`. The downstream `processPickedMedia` already handles video correctly (its `else` branch builds `UploadContent.Video`); the classifier above it just never reached that branch. The existing `isVideo` in `Videos.desktop.kt` is `desktopMain`-only and not visible from `ComposeView.kt` in `commonMain` — the structural gap that left the classifier video-blind. The inline `isVideoUri` uses the cross-platform `getFileName`, so the same fix also corrects the paste path (`onFilesPasted` at `ComposeView.kt:1378`).
+
+## Risk
+
+One file, no interface change. Image and non-media drops are bit-identical. Video extension list is now duplicated with `Videos.desktop.kt`; adding a new format means updating both — accepted as the cost of a single-file fix. iOS unaffected. Rollback: revert the commit.
diff --git a/plans/2026-05-29-fix-space-in-interface.md b/plans/2026-05-29-fix-space-in-interface.md
new file mode 100644
index 0000000000..e3d8ba7cc4
--- /dev/null
+++ b/plans/2026-05-29-fix-space-in-interface.md
@@ -0,0 +1,13 @@
+# Fix `/start remote host` parser when iface name contains a space
+
+## Problem
+
+Picking a non-default interface (e.g. Windows `Ethernet 2`) on the "Link a mobile" screen and refreshing the QR code returns `error chat: error chat commandError Failed reading: empty`. The desktop UI sends `/start remote host new addr=… iface="Ethernet 2" port=…`; the chat backend rejects it as an unparseable command. Without a workaround the user can't pin a specific local interface for the mobile-link controller.
+
+## Cause
+
+`rcCtrlAddressP` parses the iface value with `jsonP <|> text1P` (`src/Simplex/Chat/Library/Commands.hs:5549`). `jsonP` calls `A.takeByteString`, consuming *all* remaining input, then runs `eitherDecodeStrict'`. When `port=…` follows `iface=…` the strict decode fails because the JSON value `"Ethernet 2"` has trailing junk after it, so attoparsec backtracks to `text1P` (`takeTill (== ' ')`). `text1P` stops at the first space — inside the JSON quotes — leaving `2" port=12345` which nothing downstream can consume, `A.endOfInput` fails, the whole `A.choice` exhausts and surfaces attoparsec's `empty` message. With an iface name that has no space (`"lo"`) the bug is invisible: text1P swallows the full quoted token and the rest parses, but the interface name is stored with literal quotes so the iface preference silently never matches a real adapter anyway.
+
+## Fix
+
+Replace `jsonP` with a bounded `quotedP` that consumes only the bytes between `"…"` and leaves trailing fields for the next parser. `text1P` is kept as the unquoted fallback. Two-line change in `Commands.hs` plus a pure regression test in `tests/RemoteTests.hs` that asserts `parseChatCommand` of `/start remote host new addr=192.168.1.5 iface="Ethernet 2" port=12345` produces `RCCtrlAddress _ "Ethernet 2"` with port `12345`.
diff --git a/plans/2026-06-01-supporter-badges-v1.md b/plans/2026-06-01-supporter-badges-v1.md
new file mode 100644
index 0000000000..29a47a103e
--- /dev/null
+++ b/plans/2026-06-01-supporter-badges-v1.md
@@ -0,0 +1,80 @@
+# Supporter Badges v1 - Verification
+
+Badge verification in stable so that v6.5 users can see and verify badges from v7 users. Badge purchase and issuance is v2.
+
+## Why BBS+
+
+BBS+ signatures (IETF draft-irtf-cfrg-bbs-signatures) allow a holder of a signed credential to generate zero-knowledge proofs that selectively disclose some signed attributes while hiding others. Each proof uses a random nonce, making different proofs from the same credential computationally unlinkable - a verifier seeing two proofs cannot determine they came from the same credential. This means a supporter badge shown to different contacts cannot be correlated, preserving SimpleX's unlinkable identity model.
+
+The server that signs the credential sees the master secret during signing but cannot link any received proof back to any signing session - this is the core zero-knowledge property.
+
+## References
+
+- IETF draft: https://datatracker.ietf.org/doc/draft-irtf-cfrg-bbs-signatures/
+- libbbs: https://github.com/Fraunhofer-AISEC/libbbs (Apache-2.0, Fraunhofer-AISEC)
+- blst: https://github.com/supranational/blst (Apache-2.0, audited by NCC Group) - internal dependency of libbbs for BLS12-381 curve operations
+
+Both are vendored verbatim into simplexmq so that users and maintainers can verify the source matches upstream. Only libbbs API is called directly.
+
+## Crypto
+
+3 signed messages: `[ms, expiry, level]`. `ms` undisclosed (index 0), `expiry` and `level` disclosed (indexes 1, 2). Proof size: 304 bytes (272 base + 32 per undisclosed).
+
+Server public key (`srvPK`, 96 bytes) hardcoded in app.
+
+## libbbs integration
+
+Vendor libbbs + blst C sources into simplexmq. Haskell FFI bindings following the SNTRUP761 pattern (`Simplex.Messaging.Crypto.BBS.Bindings`).
+
+Full FFI surface for testing the complete flow:
+
+- `bbs_keygen_full` - generate keypair
+- `bbs_sign` - sign messages
+- `bbs_proof_gen` - generate ZK proof with selective disclosure
+- `bbs_proof_verify` - verify proof
+- `bbs_sha256_ciphersuite` - ciphersuite constant
+
+Unit tests: keygen, sign, proof gen, proof verify roundtrip. Verify proof size. Verify rejection of tampered proofs. Verify two proofs from same credential don't correlate (different presentation headers produce different proofs that both verify).
+
+Use blst portable C fallback for now (avoids per-arch assembly).
+
+## Profile type
+
+Add optional `badge` field to `Profile`. The `SupporterBadge` type uses base64-encoded newtypes for binary fields, following the `KEMPublicKey`/`KEMCiphertext` pattern from SNTRUP761 bindings:
+
+```haskell
+data SupporterBadge = SupporterBadge
+ { proof :: BBSProof
+ , proofNonce :: ByteString
+ , badgeExpiry :: UTCTime
+ , badgeType :: Text
+ }
+```
+
+`badgeType` is a string: `"supporter"`, `"business"`, `"legend"`, `"cf_investor"`. Displayed in UI as Supporter, Business, Legend, Crowdfunding Investor. `BBSProof` is a newtype over `ByteString` with `StrEncoding` instances for base64url JSON encoding.
+
+Backward compatible: `omitNothingFields` means older clients ignore it, newer clients without badge send `Nothing`.
+
+## DB
+
+- `badge` fields on `contact_profiles` and `group_member_profiles` to store received badge data
+- `badge_status` column on `contacts` and `group_members` to store verification result
+- `badge` fields on user profile (`users` or `contact_profiles` for own profile) for when badge issuance is added in v2
+
+## Verification
+
+On receiving profile with `badge` (in Subscriber.hs, `XInfo`/`XGrpMemInfo`/`XContact` handlers):
+
+1. `bbs_proof_verify(srvPK, proof, "", proofNonce, disclosed=[1,2], [expiry, level])`
+2. Check `expiry >= now`
+3. Store badge + verification status on contact/member
+
+## UI
+
+Badge icon next to display name for verified contacts/members. Different icons per level string. Expired badges shown differently or hidden.
+
+## Not in v1
+
+- Badge purchase, issuance, credential storage, proof generation - v2
+- Service framework - v2
+- Payment platform integration - v2
diff --git a/plans/2026-06-04-channel-message-signing.md b/plans/2026-06-04-channel-message-signing.md
new file mode 100644
index 0000000000..f3828018b9
--- /dev/null
+++ b/plans/2026-06-04-channel-message-signing.md
@@ -0,0 +1,233 @@
+# Plan: optional signing of channel content messages (`XMsgNew` / `XMsgUpdate`)
+
+## Goal / user problem
+
+In relay-based channels, content (`XMsgNew`) is forwarded by relays and is **not** signed today (only group-state events are — `requiresSignature`, `Protocol.hs:1251`), so a relay can forge or alter content attributed to a member. This feature lets a member *optionally* attach their member signature, so recipients holding the (signed) roster can verify authorship + integrity.
+
+Decisions:
+- **UI: both** — device-stored default ("sign my channel messages", off) + per-send long-press override (mirrors custom disappearing-message TTL).
+- **Default: off**, with an in-UI tradeoff explanation (signing = non-repudiable, transferable proof of authorship).
+- **Recipient indicator: in scope** (iOS + Kotlin) — signing is useless if invisible to readers.
+- **Event scope: `XMsgNew` + `XMsgUpdate` only**; edits reuse the original's setting. `XMsgReact`/`XMsgDel` stay unsigned in v1.
+
+## Prerequisites / sequencing
+
+Lands after #7017 (signed roster) and #7048 (roster over inline files; `GRMember` role). Neither merged yet (branch `f/allow-sign-new-msg`; `git log` tops at #7043). Dependency is specific: *verification* needs the sender's member public key, distributed via the roster; without it a signed message degrades to `MSSSignedNoKey` rather than `MSSVerified`. Integration tests must use the roster/channel setup from those PRs.
+
+**Line numbers are pre-rebase** (grounded against #7043); #7017/#7048 shift every anchor, so **re-locate by symbol**. The dependency PRs add no 6th `updateGroupChatItem` caller, but other branches are queued (`f/channel-comments`, `f/public-groups-members-in-roster`) — hence the caller re-check gate below.
+
+## What already exists (so the change stays small)
+
+Wire format, signing, verification, DB persistence, and CLI display are present and reused unchanged:
+- **Send signing:** `groupMsgSigning` (`Internal.hs:1963`) → `createSndMessages` threads `Maybe MsgSigning` (`:1950`) → `createNewSndMessage` Ed25519-signs `encodeChatBinding CBGroup (publicGroupId, memberId) <> msgBody`, storing `SignedMsg` in `SndMessage.signedMsg_` (`Store/Messages.hs:234`; `Messages.hs:1156`).
+- **Wire:** `batchMessages` prepends the signature via `encodeBatchElement` (`Batch.hs:46,65`); relay groups always batch (`memberSendAction` → only `MSASendBatched` under `useRelays'`, `Internal.hs:2222,2228`).
+- **Receive verify:** `withVerifiedMsg` (`Subscriber.hs:3469`) runs for all group messages (`:1004`, forwarded `:3431`); `XMsgNew_`/`XMsgUpdate_` ∉ `requiresSignature` ⇒ `signatureOptional` (`:3491`), so signed → `MSSVerified`/`MSSSignedNoKey`, unsigned → accepted. **No protocol-version bump.**
+- **Sent-item persistence:** `createNewSndChatItem` sets `msgSigned = MSSVerified <$ signedMsg_` (`Store/Messages.hs:550`) — own item auto-marked, readable by the edit path.
+- **Received-item persistence:** `createNewRcvChatItem` records `RcvMessage.msgSigned` (`Store/Messages.hs:565,567`); `CIMeta.msgSigned :: Maybe MsgSigStatus` (`Messages.hs:520`).
+- **CLI:** `sigStatusStr` (`View.hs:388`) appends `" (signed)"` / `" (signed, no key to verify)"`.
+
+Missing: (1) the *decision* to sign content (`groupMsgSigning` returns `Nothing` for content today); (2) per-send plumbing from the API; (3) reuse on edit; (4) the §7 stale-badge fix; (5) the §5 anonymity gate (HIGH); (6) the apps.
+
+## Threat model
+
+Actors: member (sender), recipients, and **chat relays** that forward content + roster. Relays are untrusted for content authenticity.
+
+- **Forgery of member content.** Signing closes it for signed messages: relay lacks the Ed25519 key; signature binds `(publicGroupId, memberId, body)` — no forgery, cross-bind, or alteration.
+- **Downgrade / stripping (residual, by design).** Optional signing lets a relay strip a signature and deliver unsigned. Absence of a badge is **not** proof of forgery — only *presence* of a verified badge is a guarantee. A future "required signing" group setting would close it; out of scope.
+- **Stale-badge spoof on edits (fixed — §7).** An in-place edit must not keep a `verified` badge over content from an unsigned, relay-forged `XMsgUpdate`.
+- **Publish-as-channel de-anonymization (structurally prevented — §5).** Channels allow "publish as the channel" (`showGroupAsSender`/`asGroup`): subscribers see a post as *from the channel*, not the specific owner (Design Objective 6, `docs/protocol/channels-overview.md:214`); today a relay revealing the owner is only a *deniable* leak (`channels-overview.md:~237`). `groupMsgSigning` (`Internal.hs:1963-1967`) is blind to `showGroupAsSender`, so it would sign with binding `(publicGroupId, ownerMemberId)`, broadcast on the wire even for `FwdChannel` (`encodeFwdElement` → `encodeBatchElement signedMsg_`, `Batch.hs:108`). A malicious relay sets the live-forward `fwdSender` freely (it is derived from stored `sentAsGroup`, `Store/Delivery.hs:158`), so every subscriber verifies it as `MSSVerified` — turning the deniable leak into **non-repudiable proof** of which owner authored an intentionally anonymous post; the device-default toggle would trigger this silently. For an anonymity property this must be structurally impossible: signing is never applied to as-channel content (§5), the app option is hidden for as-channel sends (§C), and a defense-in-depth guard keeps `encodeFwdElement` signature-free for `FwdChannel` (Edge cases). (`processContentItem:1302` is the *history* path and rebuilds content unsigned — not the vector.)
+- **Non-repudiation (tradeoff, by design).** A verified signature is transferable proof of authorship — a deniability loss; hence opt-in/off-by-default with UI explanation. For *as-channel* posts the loss is unacceptable, not a tradeoff — hence the §5 exclusion.
+- **What "verified" means.** Signed input is `encodeChatBinding CBGroup (publicGroupId, memberId) <> msgBody`, with `msgBody` embedding `sharedMsgId`, `MsgScope`, content (`Store/Messages.hs:242`). It proves **authorship + integrity + group/member/scope/message binding** — and nothing else: not `fwdBrokerTs` (relay-controlled, `Protocol.hs:382-387`), ordering, or completeness. Surface this in UI/help.
+- **Signed content is still relay-suppressible.** `XMsgDel_` ∉ `requiresSignature` (`Protocol.hs:1252-1262`), so an unsigned relay-forged owner-attributed delete is accepted (role-based check vs. the relay-chosen author, `Subscriber.hs:~2269`). Pre-existing, within the relay's drop power; bounds signing's value (proves *what was said*, not that all is delivered).
+- **Replay.** Binding covers `sharedMsgId` + `MsgScope`; cross-scope/group replay is blocked, same-message replay is a dedup duplicate.
+- **Bad-signature spam (fail-closed, pre-existing).** Failed verification drops content with an `RGEMsgBadSignature` item per occurrence (`Subscriber.hs:3473-3475,3483`); a tampering relay can spam these. Inherited from state-event behavior.
+
+## Core changes (Haskell)
+
+### 1. Signable-content predicate
+
+`Protocol.hs`, next to `requiresSignature` (`:1251`):
+```haskell
+-- | Content events whose authorship a member may optionally prove by signing.
+signableContent :: CMEventTag e -> Bool
+signableContent = \case
+ XMsgNew_ -> True
+ XMsgUpdate_ -> True
+ _ -> False
+```
+
+### 2. Signing decision carries the opt-in
+
+Named type near `MsgSigning` (`Protocol.hs:426`) — not a bare `Bool`:
+```haskell
+-- | Whether opt-in content signing applies to this group send.
+-- Independent of mandatory state-event signing (requiresSignature),
+-- which always applies in relay groups regardless of this value.
+data ContentSig = SignContent | DontSignContent
+ deriving (Eq, Show)
+```
+Extend `groupMsgSigning` (`Internal.hs:1963`):
+```haskell
+groupMsgSigning :: ContentSig -> GroupInfo -> ChatMsgEvent e -> Maybe MsgSigning
+groupMsgSigning csig gInfo@GroupInfo {membership = GroupMember {memberId}, groupKeys = Just GroupKeys {publicGroupId, memberPrivKey}} evt
+ | useRelays' gInfo && shouldSign =
+ Just $ MsgSigning CBGroup (smpEncode (publicGroupId, memberId)) KRMember memberPrivKey
+ where
+ tag = toCMEventTag evt
+ shouldSign = requiresSignature tag || (csig == SignContent && signableContent tag)
+groupMsgSigning _ _ _ = Nothing
+```
+- `useRelays'`/`groupKeys = Just` guards unchanged: in non-relay groups or keyless members, `SignContent` is a no-op (`Nothing`).
+- Mandatory state-event signing unaffected (`requiresSignature` branch preserved).
+
+### 3. Thread `ContentSig` through the send functions
+
+`groupMsgSigning` is called only in `sendGroupMessages_` (`Internal.hs:2134`) and `sendGroupMemberMessages` (`:1972`). Add a `ContentSig` param to `sendGroupMessages_` (`:2132`, used in `idsEvts`), `sendGroupMessages` (`:2100`, pass-through), `sendGroupMessage` (`:2088`, pass-through). Keep `sendGroupMessage'` (`:2094`) and `sendGroupMemberMessages` (`:1969`) unchanged by hardcoding `DontSignContent` internally.
+
+Behavior-preserving (all existing callers pass `DontSignContent`) ⇒ its own commit. Call sites to pass `DontSignContent` (grep-verified):
+- `sendGroupMessages`: `Subscriber.hs:1370`; `Commands.hs:793,800,2778,2909`.
+- `sendGroupMessage`: `Commands.hs:889,2690,3272,3812,3815,3819`.
+- `sendGroupMessages_` direct: `Commands.hs:2826,3849`.
+
+The two variable-`ContentSig` sites are the feature (next commit): content send (`Commands.hs:4405`) and group edit (`Commands.hs:732`).
+
+### 4. API: per-send `sign` flag
+
+Add a field to `APISendMessages` (`Controller.hs:332`):
+```haskell
+| APISendMessages {sendRef :: SendRef, liveMessage :: Bool, ttl :: Maybe Int, signMessages :: Bool, composedMessages :: NonEmpty ComposedMessage}
+```
+Parser (`Commands.hs:5006`), mirroring `liveMessageP`/`sendMessageTTLP`, defaulting off so old command strings still parse:
+```haskell
+"/_send " *> (APISendMessages <$> sendRefP <*> liveMessageP <*> sendMessageTTLP <*> signMessagesP <*> (" json " *> jsonP <|> " text " *> composedMessagesTextP))
+-- with: signMessagesP = " sign=" *> onOffP <|> pure False (place after sendMessageTTLP, before " json ")
+```
+Wire: `/_send live=.. ttl=.. sign=on|off json ...`. Per-send granularity (like `ttl`), not per-`ComposedMessage`. API boundary (app↔core, same bundle) ⇒ not a protocol-compat concern.
+
+### 5. Content send path
+
+`sendGroupContentMessages` (`Commands.hs:4366`) and `sendGroupContentMessages_` (`:4375`) gain a `ContentSig` param. `showGroupAsSender` is in scope at the send site (`:4405`); **as-channel posts are never signed** (anonymity gate — see threat model):
+```haskell
+let csig' = if showGroupAsSender then DontSignContent else csig
+(msgs_, gsr) <- sendGroupMessages user gInfo Nothing showGroupAsSender recipients csig' chatMsgEvents
+```
+This gate is structural (must live here, not only in UI); it also keeps the sender's own as-channel item unsigned and keeps §6 edit-reuse consistent.
+
+- `APISendMessages` handler (`:637-650`): `signMessages` → `SignContent`/`DontSignContent`, passed down (both `SRGroup` and `SRDirect`; direct ignores it — `sendContactContentMessages` doesn't sign). The `:4405` gate then forces `DontSignContent` for as-channel sends regardless of the flag.
+- `APIReportMessage` (`:679`): `DontSignContent` (reports unsigned in v1).
+
+### 6. Edit / restore reuse (the `XMsgUpdate` requirement)
+
+Group edit, `Commands.hs:710-742`. Own sent item loaded with `CIMeta` at `:720`; add `msgSigned` to the pattern and reuse it:
+```haskell
+... meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable, showGroupAsSender, msgSigned}
+...
+let reuseSig = if isJust msgSigned then SignContent else DontSignContent
+SndMessage {msgId} <- sendGroupMessage user gInfo scope recipients reuseSig event
+```
+`msgSigned` is loaded via `mkCIMeta`/`toGroupChatItem` (`Store/Messages.hs:2412`); for own items it is `Just MSSVerified` iff signed (`createNewSndChatItem` stores only `MSSVerified <$ signedMsg_`, `:550`), so `isJust` is the right test. This makes an edit (including the recipient-deleted-restore case) signed exactly when the original was; it is automatically consistent with §5 (as-channel originals are never signed ⇒ edits stay unsigned).
+
+Direct edit (`:697-704`) and local edit (`:745`) need no change (never signed).
+
+### 7. Security fix: refresh `msg_signed` on in-place content update
+
+**Finding:** `updateGroupChatItem_` (`Store/Messages.hs:2755`) updates content/status/timed fields but **not `msg_signed`** (`UPDATE` at `:2760-2767`); `updatedChatItem` (`:2749`) carries the original `meta.msgSigned`. Today invisible (content never signed); once content is signed and badged, an in-place edit from an **unsigned, relay-forged `XMsgUpdate`** would keep a stale `MSSVerified` badge over attacker content.
+
+**Why pass it in:** the `MSSVerified` vs `MSSSignedNoKey` outcome is computed at receive by `withVerifiedMsg` and lives only on the chat item; the stored `messages` row holds signature bytes but not the verification *outcome*. So the status must come from receive-time `RcvMessage.msgSigned`, not be re-derived.
+
+**Fix (contained to the group helper):** add a `Maybe MsgSigStatus` param to `updateGroupChatItem` (`:2746`); after `let ci' = updatedChatItem …` (`:2749`) override `ci'`'s `meta.msgSigned`, and add `msg_signed = ?` to `updateGroupChatItem_`'s `UPDATE` (`:2755`/`:2760-2767`). `updateGroupChatItem_` is called *only* from `updateGroupChatItem` (grep), so this is self-contained. **Leave `updatedChatItem` (`:2544`) unchanged** — it serves the unsigned direct/local paths (`:2540`, `:3210`).
+
+All **five** callers pass an explicit value (no implicit "preserve"):
+- `Commands.hs:738` (sender edit): `MSSVerified <$ signedMsg_` from the returned `SndMessage` (mirrors `:550`; equals the reused setting).
+- `Subscriber.hs:2212` (recipient in-place edit — *the spoof path*): `msgSigned` from the handler's `RcvMessage msg`. Unsigned forged edit ⇒ `Nothing` ⇒ badge removed; verified ⇒ kept.
+- `Subscriber.hs:2172` (recipient restore in-place, after `saveRcvChatItem'`): same `msgSigned` from `msg`.
+- `Subscriber.hs:1152` (`mdeUpdatedCI` decryption-error marker): `Nothing` — local marker, badge correctly cleared.
+- `Subscriber.hs:1509` (`upsertBusinessRequestItem` business-chat welcome): `Nothing` — never a relay channel, safely preserves `Nothing`. (Sibling direct path `:1480` uses `updateDirectChatItem'`, unaffected.)
+
+Net: signed status is set explicitly from the source of current content in every group create/update path, so a stale badge cannot exist.
+
+### 8. Paths deliberately left unsigned
+
+- Auto-reply welcome content (`Subscriber.hs:1267` `XMsgUpdate`, `:1269` `XMsgNew`) via `sendGroupMessage'` ⇒ `DontSignContent`.
+- `XMsgReact` (`Commands.hs:889`), `XMsgDel` (`Commands.hs:792-799`): unsigned in v1. Asymmetry: a post is verifiable, its reactions/deletes are not — and a signed post is still relay-suppressible (threat model). Later, extending `signableContent` could let recipients reject unsigned deletes of signed posts.
+
+## App changes (iOS + Kotlin)
+
+### A. Decode the signature status
+- **JSON tags:** core uses `enumJSON (dropPrefix "MSS")` ⇒ `MSSVerified → "verified"`, `MSSSignedNoKey → "signedNoKey"` (lower-cases first letter). **Not** the DB/text strings (`"verified"`/`"no_key"`).
+- iOS: `enum MsgSigStatus: String, Decodable { case verified, signedNoKey }`; add `public var msgSigned: MsgSigStatus?` to `CIMeta` (`apps/ios/SimpleXChat/ChatTypes.swift:3721-3737`).
+- Kotlin: `@Serializable enum class MsgSigStatus { @SerialName("verified") Verified, @SerialName("signedNoKey") SignedNoKey }`; add `val msgSigned: MsgSigStatus? = null` to `CIMeta` (`apps/multiplatform/.../model/ChatModel.kt:3434-3450`).
+- Optional field ⇒ backward-safe decode of old core JSON.
+
+### B. Device preference (default off)
+- iOS: `@AppStorage(DEFAULT_PRIVACY_SIGN_CHANNEL_MESSAGES) private var signChannelMessages = false` + toggle in `PrivacySettings.swift` (pattern: `protectScreen`, `:68-70`) with a non-repudiation footer.
+- Kotlin: `val privacySignChannelMessages = mkBoolPreference(SHARED_PREFS_PRIVACY_SIGN_CHANNEL_MESSAGES, false)` (`SimpleXAPI.kt:314`; declarations near `:122-125`) + `SettingsPreferenceItem` in `PrivacySettings.kt` with explanation.
+- App-side only (like `customDisappearingMessageTime`), not core `AppSettings`.
+
+### C. Composer option (per-send override) + thread `sign` to the API
+- Change the send closure to `(_ ttl: Int?, _ sign: Bool?)` (iOS `SendMessageView.swift:21`; Kotlin `SendMsgView.kt:54`), `sign == nil` ⇒ use device default; composer passes effective `sign = override ?? default`.
+- Long-press item next to "Disappearing message" (iOS `SendMessageView.swift:224-247`; Kotlin `SendMsgView.kt:198-209`): "Sign message" (default off) / "Send without signing" (default on).
+- **Gate visibility** on relay channel + membership has a signing key + **not as-channel** (the UI half of §5 — never offer it for as-channel publication). If app `GroupInfo` lacks relay/key state, add a derived `memberSigningAvailable` boolean to its JSON; AND it with the composer's as-channel state. Mirror `timedMessageAllowed`.
+- `apiSendMessages`: add `sign: Bool`, append `sign=on|off` — iOS `ChatCommand.apiSendMessages` (`AppAPITypes.swift:48`, encode `:239`) + `SimpleXAPI.swift:545`; Kotlin `CC.ApiSendMessages` (`SimpleXAPI.kt:3676`, encode `:3867`) + `SimpleXAPI.kt:1097`.
+
+### D. Recipient indicator
+- Show a "signed by author" indicator when `meta.msgSigned == .verified` in the meta row: iOS `CIMetaView.swift` `ciMetaText` (`:93-160`); Kotlin `CIMetaView.kt` `CIMetaText` (`:67-115`) + update `reserveSpaceForMeta` (`:118-175`) for icon width.
+- `signedNoKey`: show muted or nothing so it isn't read as `verified` (design). Surface the "verified ≠ timestamp/ordering/completeness" caveat (threat model) in help.
+- Own signed items use the same indicator (core sets `MSSVerified` on signed sends).
+
+## Compatibility analysis
+- **Protocol wire format:** unchanged; existing batch-element signature prefix. No `chatVRange` bump; pre-feature relay-capable peers verify/accept correctly.
+- **API command:** `sign=` additive with default; app+core ship together.
+- **DB:** no migration. `chat_items.msg_signed` exists (added `M20260222_chat_relays`; in both schema files; written by `createNewChatItem_:603`).
+- **App JSON:** new optional `msgSigned` decodes as absent on older cores.
+
+## Edge cases, races, correctness
+- **Member without keys** (`groupKeys = Nothing`): `groupMsgSigning` returns `Nothing` even with `SignContent` ⇒ silent unsigned send. UI gate should prevent offering it; document the silent degrade.
+- **Non-relay groups:** `useRelays'` guard ⇒ never signed; UI must not offer it.
+- **Live messages:** initial `XMsgNew` then repeated `XMsgUpdate`, each reusing the item's `msgSigned` ⇒ every increment signed. Extra cost per keystroke-batch; acceptable.
+- **Separate (non-batched) path drops signatures** (`sndMessageMBR` uses raw `msgBody`, `Internal.hs:2199`, vs the batched path's `encodeBatchElement`). Never reached in relay groups (`memberSendAction` → `MSASendBatched`). Add a test-asserted invariant; optionally make `sndMessageMBR` use `encodeBatchElement signedMsg_` too, so routing changes can't silently drop channel signatures.
+- **Defense-in-depth: no signature on `FwdChannel`.** `encodeFwdElement` (`Batch.hs:108`) includes `signedMsg_` unconditionally; §5 makes it `Nothing` for `FwdChannel` in normal flow. Add a guard/assertion that `encodeFwdElement` carries no signature when `fwdSender = FwdChannel`, so no future upstream path can reintroduce the de-anonymization.
+- **History re-send strips signatures (badge non-determinism, by design).** Relay history catch-up rebuilds content via `prepareGroupMsg` into plain `XGrpMsgForward` events (`processContentItem`, `Internal.hs:1279-1305`) and lacks the private key ⇒ unsigned. So for the same message, a live-forward recipient sees a badge while a history-catch-up recipient does not. Graceful (absence ≠ forgery); document in UI/help and test.
+- **Concurrency:** signing/verification are pure given keys; no new shared state. Send holds `withGroupLock`; receive update runs under existing receive-loop serialization. No new races.
+
+## Tests
+
+Protocol (`tests/ProtocolTests.hs`, extending `:112-312`):
+- Round-trip signed `XMsgNew`/`XMsgUpdate` through `SignedMsg`; assert binding `CBGroup <> (publicGroupId, memberId)`; `verify` accepts the right key, rejects wrong key / altered body / altered binding.
+
+Integration (`tests/ChatTests/`, using `setupRelay`/`prepareChannel1Relay`/`createChannel1Relay`/`memberJoinChannel`, `Groups.hs:8621-8750`):
+- **Sign + verify:** `sign=on` ⇒ recipient and sender items are `(signed)` (`sigStatusStr`).
+- **Off / opt-out:** `sign=off`/default ⇒ no `(signed)`.
+- **No key:** missing roster key ⇒ `(signed, no key to verify)` (`MSSSignedNoKey`).
+- **Edit reuse:** signed message edit stays `(signed)`; unsigned stays unsigned.
+- **Edit downgrade (security):** unsigned `XMsgUpdate` for a previously-signed item (forging-relay, cf. `ChatRelays.hs:220-230`) ⇒ badge **removed** (§7).
+- **As-channel never signed (anonymity):** owner posts `as_group=on sign=on` ⇒ no item is `(signed)` and no signature on the wire/stored message (guards §5).
+- **History downgrade:** live-forward recipient sees `(signed)`; later history-catch-up recipient sees the same message without it (Edge cases).
+- **Forgery rejection:** mismatched-binding replay/fabrication ⇒ signature stripped / `RGEMsgBadSignature`.
+
+App: minimal decode test that `"verified"`/`"signedNoKey"` parse to the right enum on both platforms (guards the §A tag mismatch).
+
+## Commit / diff plan
+
+1. **Structural (behavior-preserving):** add `ContentSig`, `signableContent`, parameterize `groupMsgSigning` + the three send functions, update all callers with `DontSignContent`. Reviewable as "no behavior change".
+2. **Security fix (independent, behavioral no-op today):** add `Maybe MsgSigStatus` to `updateGroupChatItem`, override `meta.msgSigned` after `updatedChatItem`, add `msg_signed` to `updateGroupChatItem_`'s `UPDATE`, update all five callers (§7). Until commit 3 every call passes `Nothing`/unchanged, so no observable change yet — but correct on its own, with a regression test that bites once signing exists.
+3. **Feature behavior (core):** `APISendMessages` field + parser; content send and edit pass the real `ContentSig` (with the §5 as-channel gate); report path `DontSignContent`.
+4. **App — decode + recipient indicator.**
+5. **App — device preference + composer option + `apiSendMessages` wiring.**
+6. **Tests** (protocol + integration) — may accompany commits 2/3.
+
+Each commit builds and passes tests independently (bisect/rollback).
+
+### Pre-implementation gates (after rebasing onto #7017 + #7048)
+- **MUST:** the as-channel gate (`showGroupAsSender ⇒ DontSignContent`, §5) lives in the *core* send path, and the app option is hidden for as-channel sends (§C) — not UI-only.
+- **MUST:** re-run `grep -rn 'updateGroupChatItem\b'` and confirm **every** caller passes an explicit `Maybe MsgSigStatus` — a missed caller silently re-introduces the §7 spoof. (Pre-rebase set: `Commands.hs:738`; `Subscriber.hs:1152,1509,2172,2212`.)
+- **SHOULD:** re-run the `sendGroupMessages`/`sendGroupMessage`/`sendGroupMessages_` caller greps; only content-send and edit pass a variable `ContentSig`, all others `DontSignContent`.
+- **SHOULD:** the three "verified"-meaning caveats (no timestamp/ordering; history downgrade; relay-suppressible) are surfaced in UI/help, and the history-downgrade test exists.
+
+## Out of scope / future
+- Group-level "expected/required signing" owner setting (closes the optional-downgrade gap).
+- Signing reactions/deletes; signing auto-reply content; verifiable reports (signed `MCReport`).
+
+## Open assumptions to confirm during implementation
+- App `GroupInfo` exposes relay+key state for the UI gate, or a derived boolean is added to its JSON.
+- Visual treatment of `signedNoKey` vs `verified`, and how to surface the "verified ≠ timestamp/ordering/completeness" caveat (threat model) in help.
diff --git a/plans/2026-06-04-fix-corrupted-video-upload-error.md b/plans/2026-06-04-fix-corrupted-video-upload-error.md
new file mode 100644
index 0000000000..e88e781a5d
--- /dev/null
+++ b/plans/2026-06-04-fix-corrupted-video-upload-error.md
@@ -0,0 +1,100 @@
+# Fix: IndexOutOfBoundsException when uploading media with an undecodable preview
+
+## Symptom
+
+```
+java.lang.IndexOutOfBoundsException: Index 6 out of bounds for length 6
+ at java.util.ArrayList.get(ArrayList.java:434)
+ at chat.simplex.common.views.chat.ComposeViewKt.ComposeView$sendMessageAsync(ComposeView.kt:827)
+ ...
+```
+
+The crash fires when sending a batch of picked media (e.g. 7 items) in which at least
+one item produces no preview bitmap — most commonly a corrupted or unusual video whose
+first frame cannot be extracted.
+
+## Root cause
+
+`ComposePreview.MediaPreview` carries two parallel lists that are assumed to be
+**equal-length and index-aligned**:
+
+```kotlin
+class MediaPreview(val images: List, val content: List)
+```
+
+Both consumers cross-index one list by the other's index, so the invariant is load-bearing:
+
+- `ComposeImageView` (preview row) iterates `media.images` and reads `media.content[index]`.
+- `ComposeView.sendMessageAsync` iterates `preview.content` and reads `preview.images[index]`
+ (the `MCImage` / `MCVideo` preview string). This is the crash site.
+
+`processPickedMedia` built the two lists out of step:
+
+- `imagesPreview` was appended **only when `bitmap != null`**.
+- `content` was appended **unconditionally** for videos, and for animated images that
+ passed the size check — regardless of whether a preview bitmap was produced.
+
+`getBitmapFromVideo` returns `PreviewAndDuration(null, …)` whenever Android's
+`MediaMetadataRetriever` cannot extract a frame, **even when no exception is thrown**
+(`Utils.android.kt:351`). So a single undecodable video appends to `content` but not to
+`images`, leaving `content.size == images.size + 1`. Iterating `content` then indexes
+`images[lastIndex+1]` → `Index N out of bounds for length N`.
+
+This is a pre-existing bug in the shared media picker; it is unrelated to any in-flight
+feature work and reproduces on both Android and Desktop (both use the `commonMain`
+`processPickedMedia`).
+
+## Fix
+
+Keep `content` and `imagesPreview` strictly paired at the source. The `when` now yields an
+`UploadContent?` instead of mutating `content` inside its branches, and both lists are
+appended together, gated on a non-null preview bitmap:
+
+```kotlin
+if (bitmap != null && uploadContent != null) {
+ content.add(uploadContent)
+ imagesPreview.add(resizeImageToStrSize(bitmap, maxDataSize = 14000))
+}
+```
+
+Each iteration now adds exactly zero or one entry to **both** lists, so the
+equal-length / index-aligned invariant holds by construction.
+
+### Behavior change
+
+Media that yields no decodable preview frame is now **skipped** rather than enqueued.
+Previously such a video crashed the send; now only the bad item is dropped and the rest of
+the picked batch sends normally (the loop evaluates each URI independently).
+
+The skip is **not silent**. A skipped video shows `showVideoDecodingException()`, gated on
+`AlertManager.hasAlertsShown()` so the alert neither stacks across several bad items in one
+batch nor duplicates the one `getBitmapFromVideo` already shows on its exception path. The
+genuinely silent gap this closes is the video path that returns a null frame **without**
+throwing (Android `getFrameAtTime` returns null; Desktop snapshot times out) — that path
+previously produced no alert and then crashed on send.
+
+Image decode failures need no new alert here: `getBitmapFromUri` is already called for every
+image (animated or not) with `withAlertOnException = !hasAlertsShown()`, so a null image
+bitmap is surfaced before this point. Only the video null-frame case lacked any notice.
+
+## Why this approach
+
+- **Fixes the invariant at its origin** rather than papering over it at the two read
+ sites. Guarding `images[index]` in `sendMessageAsync` would stop the crash but leave the
+ preview row (`ComposeImageView`) silently mismatched and the actual media set ambiguous.
+- **Minimal, surgical diff** confined to `processPickedMedia`; no API/type changes, no new
+ placeholder assets, no touch to the read sites.
+- **Cross-platform by construction**: the change lives in `commonMain`, so Android and
+ Desktop are both covered. iOS has a separate Swift compose implementation and is out of
+ scope for this fix.
+
+## Other `MediaPreview` construction sites (verified aligned)
+
+- `cs.preview` → single-element `listOf(mc.image)` / `listOf(content)` (edit path): aligned.
+- `constructFailedMessage` takes `last()` of each list: aligned if the input was aligned.
+
+## Test notes
+
+Manual repro: pick a multi-item batch including a corrupted/zero-frame video and send.
+- Before: `IndexOutOfBoundsException` on send.
+- After: the undecodable item is dropped; remaining media sends normally.
diff --git a/plans/2026-06-09-perf-group-members-merge-on2.md b/plans/2026-06-09-perf-group-members-merge-on2.md
new file mode 100644
index 0000000000..3000103b05
--- /dev/null
+++ b/plans/2026-06-09-perf-group-members-merge-on2.md
@@ -0,0 +1,95 @@
+# Perf — index member merge in `setGroupMembers` to O(n)
+
+**PR:** #7061 (`nd/group-members-merge-on2`)
+**Scope:** client-only (multiplatform: android + desktop). One-line change, no behavioral change.
+**File:** `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt:254`
+
+## Root cause (verified)
+
+`setGroupMembers` is the shared loader for the in-memory member list. After fetching
+members from core (`apiListMembers`) it merges the freshly-loaded list with the
+connection stats already held in memory, so an in-flight `connectionStats` isn't lost
+when the list is reloaded — `ChatListNavLinkView.kt:257-267`:
+
+```kotlin
+val currentMembers = chatModel.groupMembers.value
+val newMembers = groupMembers.map { newMember ->
+ val currentMember = currentMembers.find { it.id == newMember.id } // O(n) scan, inside an O(n) map → O(n²)
+ ...
+}
+```
+
+Two compounding costs:
+
+1. **O(n²) merge.** `currentMembers.find { it.id == newMember.id }` is a linear scan
+ run once per new member — `n` lookups × `n` scan = O(n²).
+2. **~n² String allocations + GC pressure.** `GroupMember.id` is a *computed* property,
+ not a stored field — `ChatModel.kt:2424`:
+
+ ```kotlin
+ val id: String get() = "#$groupId @$groupMemberId"
+ ```
+
+ Every `it.id` and `newMember.id` access allocates a fresh `String`. The nested
+ `find` evaluates `it.id` for (worst case) every current member on every iteration,
+ so the merge allocates on the order of n² short-lived strings, each compared by
+ value. In groups with thousands of members this is a visible main-thread lag spike.
+
+## Worst case observed
+
+`setGroupMembers` reloads `chatModel.groupMembers` (and runs the merge) whenever the
+member list is (re)loaded while members are already in memory. The most noticeable
+case: the **Chats with members** support-chat modal (`MemberSupportView`) reloads the
+whole list via `LaunchedEffect(Unit) { setGroupMembers(...) }` every time it (re)enters
+composition — e.g. after reading and closing a member's support chat — so it pays the
+full O(n²) merge and produces a lag spike on close in large groups
+(`MemberSupportView.kt:44`).
+
+## The fix (minimal — one change)
+
+Index the current members by id **once**, then look up in O(1):
+
+```kotlin
+val currentMembersById = chatModel.groupMembers.value.associateBy { it.id }
+val newMembers = groupMembers.map { newMember ->
+ val currentMember = currentMembersById[newMember.id]
+ ...
+}
+```
+
+- `associateBy { it.id }` builds the index in a single O(n) pass; each subsequent
+ lookup is O(1). Total merge cost drops from O(n²) to O(n).
+- String allocations drop from ~n² to ~2n (one `it.id` per current member while
+ building the map, one `newMember.id` per lookup).
+
+## Why it's safe (no behavioral change)
+
+- **Member ids are unique** — `id = "#$groupId @$groupMemberId"` is unique per member
+ within a group, and `setGroupMembers` always works within a single `groupInfo`. So
+ `associateBy { it.id }` cannot collide; `map[id]` returns exactly what
+ `find { it.id == id }` returned.
+- Same result set, same merged `GroupMember` objects, same order of `newMembers`
+ (the `map` over `groupMembers` is unchanged). Only the lookup strategy changes.
+- Everything downstream of the merge is untouched — `groupMembersIndexes`,
+ `groupMembers.value`, `membersLoaded`, `populateGroupMembersIndexes()`
+ (`ChatListNavLinkView.kt:268-271`).
+
+## Also sped up (same shared loader)
+
+`setGroupMembers` is the common in-memory member loader, called from several screens;
+all of them get the same speedup in large groups (identical results, just O(n)):
+
+- Group member list / member management — `GroupChatInfoView` (`:121, :1250, :1267`)
+- @-mention autocomplete — `GroupMentions` (`:119, :134`)
+- Channel relays — `ChannelRelaysView` (`:38, :124`)
+- Add members — `AddGroupView` (`:52`)
+- The group chat's member load on open — `ChatView` (multiple sites)
+- Chats with members — `MemberSupportView` (`:45, :67`)
+
+## Verification
+
+- Reasoned: ids unique within a group → `associateBy`/lookup is semantically identical
+ to the linear `find`. No caller observes a difference.
+- Manual: open **Chats with members** in a large group, read and close a member's
+ support chat repeatedly — the lag spike on close should be gone. Member list,
+ @-mentions, relays, and add-members screens should remain identical, just faster.
diff --git a/plans/2026-06-12-fix-migrate-text-overlap.md b/plans/2026-06-12-fix-migrate-text-overlap.md
new file mode 100644
index 0000000000..1bb1d3a5c6
--- /dev/null
+++ b/plans/2026-06-12-fix-migrate-text-overlap.md
@@ -0,0 +1,71 @@
+# Fix overlapping warning texts after finalizing migration
+
+Branch: `nd/fix-migrate-text` · regression from PR [#6777](https://github.com/simplex-chat/simplex-chat/pull/6777) (`df5ea3d46`, new settings section design).
+
+## 1. Problem statement
+
+On the "Migrate device" screen (Android and desktop), after tapping **Finalize migration** the finished state renders broken: the two warning texts — "You **must not** use the same database on two devices." and "**Please note**: using the same database on two devices will break the decryption of messages…" — are painted on top of each other and on top of the "Migration complete" section card, directly under the section header.
+
+Reproduced on desktop with default settings. The screen immediately before (`LinkShownView`, with the QR code) renders correctly.
+
+## 2. Solution summary
+
+Move the two `SectionTextFooter` calls in `FinishedView` out of the `Box` and place them after it, so they render as sequential children of the screen's scroll `Column` — the same placement `LinkShownView` already uses for its footers.
+
+```diff
+ }
+- SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_you_must_not_start_database_on_two_device))
+- SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_using_on_two_device_breaks_encryption))
+ if (chatDeletion) {
+ ProgressView()
+ }
+ }
++ SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_you_must_not_start_database_on_two_device))
++ SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_using_on_two_device_breaks_encryption))
+ }
+```
+
+Total diff: 1 file, 2 lines moved (+2 / −2 at different indentation).
+
+## 3. Root cause
+
+PR #6777 added card chrome to `SectionView` and, in a sub-commit ("Migrate views: move all SectionTextFooter / SectionSpacer out of SectionView lambdas"), moved footers out of the card lambdas so they read as captions below the cards. In `FinishedView` (`MigrateFromDevice.kt`) the footers were moved out of the `SectionView` — but left **inside the wrapping `Box`**:
+
+```kotlin
+Box {
+ SectionView(stringResource(MR.strings.migrate_from_device_migration_complete)) {
+ // "Start chat" / "Delete database" buttons
+ }
+ SectionTextFooter(…you_must_not_start_database_on_two_device…) // Box child 2
+ SectionTextFooter(…using_on_two_device_breaks_encryption…) // Box child 3
+ if (chatDeletion) {
+ ProgressView() // Box child 4 (overlay)
+ }
+}
+```
+
+That `Box` exists for exactly one reason: to overlay `ProgressView` (a fullscreen-centered spinner) over the section while the chat database is being deleted. `Box` stacks its children at `TopStart`, so both footers render at the Box's top-left corner — over the card's top edge and over each other. This is the only migration sub-view where the refactor produced this shape: the other `Box`-wrapped states keep all flow content inside one child (a `SectionView` or an inner `Column`), and `LinkShownView` has no `Box` at all.
+
+`FinishedView` is composed inside `ColumnWithScrollBar` (via `SectionByState`), so composables emitted at the function's top level land in the scroll `Column` and stack vertically — which is where the footers belong.
+
+## 4. The fix in detail, and why this shape
+
+Three candidate fixes were compared:
+
+- **Move the 2 footer lines after the `Box`** (chosen). Smallest possible diff, zero re-indentation. Footers become siblings of the Box in the scroll `Column`, identical to the working `LinkShownView` pattern in the same file. `ProgressView` keeps its overlay semantics unchanged (same as `DatabaseInitView`, `ArchivingView`, `LinkCreationView`). Only behavioural delta beyond the bug fix: during the transient `chatDeletion` spinner, the overlay centers over the card rather than card + footers — matching every other migration sub-view.
+- **Wrap card + footers in a `Column` inside the Box.** Behaviorally near-identical, but ~40 lines of indentation churn and a layout shape no sibling view uses. Rejected: larger diff, no benefit.
+- **Also hoist `ProgressView` out of the Box.** Changes overlay semantics (spinner would flow below content instead of over it). Rejected: touches behavior the bug report doesn't concern.
+
+Regression risk: the change is placement-only — no logic, no state, no measurement changes. The new arrangement is the proven pattern of the adjacent view.
+
+## 5. Scope verification — no other instances of the bug class
+
+The class ("flow content as direct children of an overlay `Box`") was searched for across all Kotlin source sets (`commonMain`, `androidMain`, `desktopMain`, `android`, `desktop`) with three complementary structural scans:
+
+1. Every `Box` block with ≥2 stacking flow children (section views, footers, spacers, settings items): **only** `FinishedView`.
+2. All 384 footer/spacer call sites classified by nearest enclosing block: the only ones directly inside a `Box` are the two fixed lines.
+3. All ~25 composable functions that emit footers at function top level (placement decided by caller): no caller invokes them inside a `Box`.
+
+iOS is structurally immune: SwiftUI footers are part of `Section { } footer: { }` inside a `List`; `MigrateFromDevice.swift`'s `finishedView` was verified correct.
+
+Related but distinct (not fixed here): 10 `SectionTextFooter` calls app-wide still sit *inside* `SectionView` card lambdas (6 in migration views, plus `LinkAMobileView`, `ConnectMobileView`, 2 in `NetworkAndServers`), rendering inside the white card instead of as captions below it. Cosmetic placement inconsistency with #6777's stated pattern, no overlap — left for a separate change if desired.
diff --git a/plans/2026-06-15-fix-cli-outdated-help.md b/plans/2026-06-15-fix-cli-outdated-help.md
new file mode 100644
index 0000000000..e0105ef5bb
--- /dev/null
+++ b/plans/2026-06-15-fix-cli-outdated-help.md
@@ -0,0 +1,42 @@
+# Remove CLI help entries for long-removed commands
+
+Branch: `nd/fix-cli-outdated-help` · file `src/Simplex/Chat/Help.hs`.
+
+## 1. Problem statement
+
+Typing `/get stats` in the terminal CLI does nothing useful — it is documented in `/help` but no parser accepts it, so it fails to parse. Investigation found this is not isolated: four documented commands no longer exist in the parser.
+
+## 2. Solution summary
+
+Remove the four stale entries (five lines, including one continuation note) from `Help.hs`:
+
+- `/pq @ on/off` + its "(both have to enable…)" note — `contactsHelpInfo`
+- `/pq on/off` — `settingsInfo`
+- `/get stats` — `settingsInfo`
+- `/reset stats` — `settingsInfo`
+
+The stats pair were the tail of `settingsInfo`, so the now-orphaned trailing comma on the preceding `/(un)mute #` element is also dropped to keep the list literal valid.
+
+No replacement text is added: PQ has no command (it is automatic), and the stats functionality has no argument-compatible successor (see §4).
+
+## 3. Root cause
+
+Both removals were core changes that deleted parser, handler, and command constructor but left `Help.hs` untouched:
+
+- **`/pq` (both forms)** — commit `756779186` "core: enable PQ encryption for contacts (#4049)", 2024-04-22. It removed the parsers `"/pq @" *> (SetContactPQ …)` and `"/pq " *> (APISetPQEncryption …)`; post-quantum encryption for contacts became automatic, so the manual toggle was obsolete. `SetContactPQ` and `APISetPQEncryption` no longer exist in `src/`.
+- **`/get stats` / `/reset stats`** — commit `5907d8bd0` "core: remove legacy agent stats (#4375)", 2024-07-01. It removed the parsers `"/get stats" $> GetAgentStats` and `"/reset stats" $> ResetAgentStats`, their handlers, the `GetAgentStats`/`ResetAgentStats` constructors in `Controller.hs`, and the `View.hs` rendering — but its diff touched `Chat.hs`, `Controller.hs`, `View.hs`, `cabal.project`, `sha256map.nix`, not `Help.hs`.
+
+In both cases the help text became a promise the binary could no longer keep.
+
+## 4. Scope verification — no other stale entries, no replacements documented
+
+All 120 commands documented across every section of `Help.hs` were extracted and matched against the parser string literals in `Library/Commands.hs` (`chatCommandP`). Every entry resolves to a live parser except the four above. ~10 entries that a naive prefix match flagged were manually confirmed valid: incognito-suffix forms parsed by `incognitoP` (`/accept incognito`, `/connect incognito`, `/simplex incognito`), usage examples (`/file bob ./photo.jpg`, `/group team`), and inline sub-alternatives (`/start remote host new`, `/stop remote host new`, `/switch remote host local`, `/chats all`).
+
+Why no replacement text:
+
+- **PQ** — there is no command; encryption is negotiated automatically. Documenting nothing is correct.
+- **Stats** — the nearest live commands are `/get servers summary ` and `/reset servers stats`, but they require a `userId` argument and return the agent servers summary, not the old argument-less usage statistics. They were never in CLI help; adding them is a separate documentation enhancement, deliberately out of scope for a "remove what no longer exists" fix.
+
+## 5. Why this shape
+
+Pure deletion of dead documentation — no behavioral change, smallest diff that makes `/help` truthful. Comma handling is the only subtlety: the `/pq @` and `/pq on/off` removals sit before comma-bearing neighbors (a `""` separator and `/network` respectively) and need no adjustment; the `/get stats` + `/reset stats` removal makes `/(un)mute #` the last `settingsInfo` element, so its trailing comma is removed to avoid a dangling-comma parse error before `]`.
diff --git a/plans/2026-06-15-fix-file-upload-long-name.md b/plans/2026-06-15-fix-file-upload-long-name.md
new file mode 100644
index 0000000000..f0917783bd
--- /dev/null
+++ b/plans/2026-06-15-fix-file-upload-long-name.md
@@ -0,0 +1,77 @@
+# Fix: long file name hides the close icon in the compose file preview
+
+Date: 2026-06-15
+Branch: `nd/fix-file-upload-with-long-name`
+Platforms affected: Android, Desktop, iOS
+
+## Problem
+
+When a file is attached for sending, the compose area shows a preview row with the
+file icon, the file name, and a close (X) icon to cancel/remove the file before
+sending. If the file name is long, the close icon is not shown, so the user cannot
+dismiss the attachment.
+
+## Cause
+
+The bug is the same layout defect on both codebases: the file-name text is
+unconstrained, so a long name consumes all horizontal space and squeezes the
+trailing close button to zero width.
+
+### Android / Desktop — `ComposeFileView.kt`
+
+The row was laid out as:
+
+```
+Icon(fixed) | Text(fileName) | Spacer(weight 1f) | IconButton(close)
+ ^ unweighted, no maxLines
+```
+
+In a Compose `Row`, unweighted children are measured first and take the remaining
+width before weighted children get anything. The unweighted `Text` therefore grabbed
+the whole remaining width on a long name, leaving the weighted `Spacer` — and the
+`IconButton` after it — with ~0 width. The flexible element was the `Spacer`, but a
+`Spacer` can only distribute the space the rigid `Text` did not already eat.
+
+### iOS — `ComposeFileView.swift`
+
+```
+Image(fixed) | Text(fileName) | Spacer() | Button(close)
+ ^ no lineLimit
+```
+
+A `Text` with no `lineLimit` reports its full single-line ideal width and refuses to
+truncate, so a long name collapses the `Spacer` and pushes the `Button` past the
+`.frame(maxWidth: .infinity)` edge, off-screen.
+
+## Fix
+
+Make the file name the element that yields space and let it truncate, so the
+fixed-size close control's space is always reserved.
+
+- **Kotlin:** give the `Text` the `weight(1f)` (instead of the `Spacer`) and
+ `maxLines = 1`, and drop the now-redundant `Spacer`. This matches the existing
+ idiom — `ComposeImageView` puts `weight(1f)` on its content, and `CIFileView`
+ caps file-name text with `maxLines = 1`.
+- **Swift:** add `.lineLimit(1)` to the `Text`, so it truncates instead of
+ overflowing, matching how file names are shown elsewhere on iOS.
+
+## Why this is the right fix (not a workaround)
+
+`ComposeFileView` was the only compose preview that gave the weight to a `Spacer`
+rather than to its content; every sibling preview (`ComposeImageView`,
+`ContextItemView`) reserves space for the trailing close control by weighting the
+content. The change brings the file preview in line with the established pattern
+rather than adding a special case. It is purely structural — no behavior changes
+beyond layout.
+
+## Scope / risk
+
+- One-spot edit per file; no API or behavior change.
+- Android and Desktop share the Kotlin file, so both are fixed together; iOS is the
+ separate Swift file.
+- No string/translation keys touched.
+
+## Verification
+
+- Visual: attach a file with a very long name on Android, Desktop, and iOS; confirm
+ the name truncates and the close (X) icon stays visible and tappable.
diff --git a/plans/2026-06-17-fix-group-garbled-error.md b/plans/2026-06-17-fix-group-garbled-error.md
new file mode 100644
index 0000000000..c660d13a48
--- /dev/null
+++ b/plans/2026-06-17-fix-group-garbled-error.md
@@ -0,0 +1,27 @@
+# Fix garbled error when saving group profile (member admission)
+
+## Problem
+
+Saving a group profile change — e.g. enabling member admission (Review = "All") from Group preferences → Member admission — can fail with an unreadable alert:
+
+```
+chat.simplex.common.model.API$Error@3ea295c.err
+```
+
+The user sees an object reference instead of the actual error, so there is no way to tell what went wrong.
+
+## Cause
+
+In `apiUpdateGroup` the `API.Error` branch builds the alert message with `"$r.err"` (`SimpleXAPI.kt:2292`). In a Kotlin string template `"$r.err"` interpolates `r.toString()` — and `API.Error` has no custom `toString`, so it yields `chat.simplex.common.model.API$Error@` — then appends the literal text `.err`. The meaningful message (`r.err.string`) is never read.
+
+This surfaces whenever the core rejects the update. A concrete trigger is a **desynced member role**: the client shows the Save controls because `groupInfo.isOwner` is true, but the core's `assertUserGroupRole gInfo GROwner` (`Commands.hs:3840`) disagrees and returns `CEGroupUserRole`. The display bug then hides which error it was.
+
+## Fix
+
+Render the error message instead of the object reference:
+
+```kotlin
+AlertManager.shared.showAlertMsg(generalGetString(errorTitle), "${r.err.string}")
+```
+
+One-line change in `SimpleXAPI.kt`. This is the only occurrence of the `"$r.err"` pattern in the codebase. The underlying core rejection is unchanged — but it is now shown clearly to the user.
diff --git a/plans/delete-leave-dialog-with-profile-impl.md b/plans/delete-leave-dialog-with-profile-impl.md
new file mode 100644
index 0000000000..860d555d36
--- /dev/null
+++ b/plans/delete-leave-dialog-with-profile-impl.md
@@ -0,0 +1,323 @@
+# Implementation plan — chat name on its own line in delete/leave/clear dialogs
+
+Follows the product spec in
+[`delete-leave-dialog-with-profile.md`](./delete-leave-dialog-with-profile.md).
+
+Pure code change — zero string additions, zero new helpers, zero
+signature changes. Each call site edits one argument: the `text =` /
+`message:` value gains `"${displayName}\n\n"` prepended to the
+existing localized warning (or, where there is no current body, the
+chat name becomes the new body).
+
+One commit per platform.
+
+## Commit 1 — Kotlin
+
+**Files touched:**
+- `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt`
+ — adds `parseHtml: Boolean = true` to `showAlertDialog` and
+ `showAlertDialogButtonsColumn`. When `false`, the body text is wrapped
+ as `AnnotatedString` and routed through the existing AnnotatedString
+ `AlertContent` overload, which does NOT call
+ `escapedHtmlToAnnotatedString`. Default stays `true` so existing
+ callers are unaffected.
+- `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt`
+- `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt`
+- `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt`
+ — adds the previously-missed `deleteContactConnectionAlert`
+ dispatcher to the coverage (pending contact connections).
+
+Every Kotlin call site that prepends the chat name sets
+`parseHtml = false`, so `displayName` is never HTML-interpreted.
+
+### 1.1 — `deleteGroupDialog` (`GroupChatInfoView.kt:182`)
+
+```diff
+ fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
+ val chatInfo = chat.chatInfo
+ val titleId = /* unchanged */
+ val messageId = /* unchanged */
+ AlertManager.shared.showAlertDialog(
+ title = generalGetString(titleId),
+- text = generalGetString(messageId),
++ text = "${groupInfo.displayName}\n\n${generalGetString(messageId)}",
+ confirmText = generalGetString(MR.strings.delete_verb),
+ onConfirm = { /* unchanged */ },
+ destructive = true,
+ )
+ }
+```
+
+### 1.2 — `leaveGroupDialog` (`GroupChatInfoView.kt:222`)
+
+```diff
+ fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
+ val titleId = /* unchanged */
+ val messageId = /* unchanged */
+ AlertManager.shared.showAlertDialog(
+ title = generalGetString(titleId),
+- text = generalGetString(messageId),
++ text = "${groupInfo.displayName}\n\n${generalGetString(messageId)}",
+ confirmText = generalGetString(MR.strings.leave_group_button),
+ onConfirm = { /* unchanged */ },
+ destructive = true,
+ )
+ }
+```
+
+Signature unchanged. No caller updates. `groupInfo.displayName` is
+already available on the existing parameter (`ChatModel.kt:2142`).
+
+### 1.3 — `clearChatDialog` (`ChatInfoView.kt:492`)
+
+```diff
+ fun clearChatDialog(chat: Chat, close: (() -> Unit)? = null) {
+ AlertManager.shared.showAlertDialog(
+ title = generalGetString(MR.strings.clear_chat_question),
+- text = generalGetString(MR.strings.clear_chat_warning),
++ text = "${chat.chatInfo.displayName}\n\n${generalGetString(MR.strings.clear_chat_warning)}",
+ confirmText = generalGetString(MR.strings.clear_verb),
+ onConfirm = { controller.clearChat(chat, close) },
+ destructive = true,
+ )
+ }
+```
+
+### 1.4 — Contact-delete dispatchers (`ChatInfoView.kt`)
+
+Four functions. `deleteContactOrConversationDialog` (line 248) has
+no existing `text =`, so the chat name becomes the new body. The
+other three already have a `text =`, so the name is prepended.
+
+All four already have `contact: Contact` as a parameter, so
+`contact.displayName` is used directly (same value as
+`chat.chatInfo.displayName` for a direct chat, shorter expression).
+
+```diff
+ // deleteContactOrConversationDialog — line 248
+ private fun deleteContactOrConversationDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)?) {
+ AlertManager.shared.showAlertDialogButtonsColumn(
+ title = generalGetString(MR.strings.delete_contact_question),
++ text = contact.displayName,
+ buttons = { /* unchanged */ }
+ )
+ }
+```
+
+```diff
+ // deleteActiveContactDialog — line 304
+ private fun deleteActiveContactDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)? = null) {
+ val contactDeleteMode = mutableStateOf(ContactDeleteMode.Full())
+ AlertManager.shared.showAlertDialogButtonsColumn(
+ title = generalGetString(MR.strings.delete_contact_question),
+- text = generalGetString(MR.strings.delete_contact_cannot_undo_warning),
++ text = "${contact.displayName}\n\n${generalGetString(MR.strings.delete_contact_cannot_undo_warning)}",
+ buttons = { /* unchanged */ }
+ )
+ }
+```
+
+Same diff for `deleteContactWithoutConversation` (line 361) and
+`deleteNotReadyContact` (line 417) — both use
+`delete_contact_cannot_undo_warning`. Neither takes `contact` as
+a parameter, so the name is read via `chat.chatInfo.displayName`
+(which resolves to `contact.displayName` because these dispatchers
+are only reached for `ChatInfo.Direct` chats). Their titles
+(`confirm_delete_contact_question`) stay unchanged — the
+not-ready / no-conversation paths keep their distinct title.
+
+## Commit 2 — iOS
+
+**Files touched:**
+- `apps/ios/Shared/Views/ChatList/ChatListNavLink.swift`
+- `apps/ios/Shared/Views/Chat/ChatInfoView.swift`
+- `apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift`
+
+### 2.1 — `deleteGroupAlert` (two locations)
+
+`Views/Chat/Group/GroupChatInfoView.swift:835` and
+`Views/ChatList/ChatListNavLink.swift:567` get the same diff.
+`deleteGroupAlertMessage(_:)` already returns a `Text` containing
+the localized warning — concatenate to it.
+
+```diff
+ private func deleteGroupAlert() -> Alert {
+ let label: LocalizedStringKey = /* unchanged */
+ return Alert(
+ title: Text(label),
+- message: deleteGroupAlertMessage(groupInfo),
++ message: Text(chat.chatInfo.displayName) + Text(verbatim: "\n\n") + deleteGroupAlertMessage(groupInfo),
+ primaryButton: .destructive(Text("Delete")) { /* unchanged */ },
+ secondaryButton: .cancel()
+ )
+ }
+```
+
+`Text(chat.chatInfo.displayName)` resolves to `Text(_ content: some StringProtocol)`
+(the runtime-string overload — no localization lookup, matches
+codebase convention: `ChatView.swift:984`, `ChatInfoToolbar.swift:49`,
+`SettingsView.swift:540`). `Text(verbatim: "\n\n")` is the literal
+separator, matching the codebase convention that reserves
+`verbatim:` for fixed punctuation (`ContextItemView.swift:88` is
+the textbook example: `Text(chatLink.displayName) + Text(verbatim: " - ")`).
+The third term `Text(messageLabel)` keeps the existing
+`LocalizedStringKey` lookup.
+
+### 2.2 — `leaveGroupAlert` (two locations)
+
+`Views/Chat/Group/GroupChatInfoView.swift:872` and
+`Views/ChatList/ChatListNavLink.swift:622`:
+
+```diff
+ private func leaveGroupAlert() -> Alert {
+ let titleLabel: LocalizedStringKey = /* unchanged */
+ let messageLabel: LocalizedStringKey = /* unchanged */
+ return Alert(
+ title: Text(titleLabel),
+- message: Text(messageLabel),
++ message: Text(chat.chatInfo.displayName) + Text(verbatim: "\n\n") + Text(messageLabel),
+ primaryButton: .destructive(Text("Leave")) { /* unchanged */ },
+ secondaryButton: .cancel()
+ )
+ }
+```
+
+### 2.3 — `clearChatAlert` (three locations)
+
+`Views/Chat/ChatInfoView.swift:577`,
+`Views/Chat/Group/GroupChatInfoView.swift:858`,
+`Views/ChatList/ChatListNavLink.swift:600`:
+
+```diff
+ private func clearChatAlert() -> Alert {
+ Alert(
+ title: Text("Clear conversation?"),
+- message: Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."),
++ message: Text(chat.chatInfo.displayName) + Text(verbatim: "\n\n") + Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."),
+ primaryButton: .destructive(Text("Clear")) { /* unchanged */ },
+ secondaryButton: .cancel()
+ )
+ }
+```
+
+### 2.4 — Contact-delete action sheets
+
+Three functions in `Views/Chat/ChatInfoView.swift`. None currently
+pass `message:` to `ActionSheet`; we add it. `ActionSheet`'s
+`message:` is an optional second parameter that SwiftUI already
+supports.
+
+All three functions have `contact: Contact` in scope. Use bare
+`Text(contact.displayName)` (resolves to the `StringProtocol`
+overload, no localization lookup, matches codebase convention).
+Add only the name as `message:` — these ActionSheets had no
+message before, so adding any additional warning would be new
+behavior beyond the stated goal.
+
+**`deleteContactOrConversationDialog`** (line 1177):
+
+```diff
+ private func deleteContactOrConversationDialog(
+ _ chat: Chat, _ contact: Contact, _ dismissToChatList: Bool,
+ _ showAlert: @escaping (SomeAlert) -> Void,
+ _ showActionSheet: @escaping (SomeActionSheet) -> Void,
+ _ showSheetContent: @escaping (SomeSheet) -> Void
+ ) {
+ showActionSheet(SomeActionSheet(
+ actionSheet: ActionSheet(
+ title: Text("Delete contact?"),
++ message: Text(contact.displayName),
+ buttons: [ /* unchanged */ ]
+ ),
+ id: "deleteContactOrConversationDialog"
+ ))
+ }
+```
+
+**`deleteContactWithoutConversation`** (line 1324):
+
+```diff
+ showActionSheet(SomeActionSheet(
+ actionSheet: ActionSheet(
+ title: Text("Confirm contact deletion?"),
++ message: Text(contact.displayName),
+ buttons: [ /* unchanged */ ]
+ ),
+ id: "deleteContactWithoutConversation"
+ ))
+```
+
+**`deleteNotReadyContact`** (line 1348) — same:
+
+```diff
+ showActionSheet(SomeActionSheet(
+ actionSheet: ActionSheet(
+ title: Text("Confirm contact deletion?"),
++ message: Text(contact.displayName),
+ buttons: [ /* unchanged */ ]
+ ),
+ id: "deleteNotReadyContact"
+ ))
+```
+
+### 2.5 — `DeleteActiveContactDialog` sheet (line 1282) unchanged
+
+The secondary multi-option sheet is reached only after the user
+confirms "Delete contact" in the previous action sheet — which now
+shows the name. The sheet itself remains as-is.
+
+## Verification
+
+For each platform, exercise every entry point and confirm the
+body reads `` on its own line followed by the existing
+warning:
+
+- Android & Desktop:
+ - Chat list swipe — direct contact, group, channel, business chat
+ → delete / clear / leave. (Note folder's clear dialog is
+ intentionally unchanged — `clearNoteFolderDialog` excluded.)
+ - Chat info screens — "Delete contact" / "Delete group" / "Delete
+ channel" / "Clear conversation" / "Leave …" rows.
+ - Contact list (`ContactListNavView.kt:148`) — "Delete contact"
+ action shows the name in entry-point dialog and toggle dialog.
+ - Multi-option contact-delete path: entry dialog (now has a name
+ body where it had none) → toggle dialog (name above the
+ warning) → success.
+- iOS:
+ - Same matrix from chat list swipe and chat info screens.
+ - Action-sheet contact-delete paths show the name as the
+ `message:` line on iPhone and iPad.
+
+Edge cases:
+
+- Long chat name — alert containers wrap automatically; the body
+ occupies 3+ lines. Confirm with a chat renamed to ~40 characters.
+- Special characters (emoji, RTL, double quotes) — render literally
+ via string interpolation, no format-substitution involved.
+- Empty `displayName` — does not occur in practice (`NamedChat`
+ enforces non-empty via `localAlias.ifEmpty { profile.displayName }`).
+
+Diff-level checks:
+
+- `git diff '*strings.xml' '*Localizable.strings'` returns zero
+ hunks. Pure code change.
+- `git diff --stat` shows ~5 files total: two Kotlin dispatcher
+ files, three iOS view files.
+- Cancel/confirm flows behave exactly as before — same API calls,
+ same model updates, same navigation.
+
+## Out of scope
+
+- Profile picture / avatar in dialogs — excluded by product decision.
+- Refactoring the iOS duplication between `ChatListNavLink` and
+ `GroupChatInfoView` / `ChatInfoView` (pre-existing `// TODO` at
+ `GroupChatInfoView.swift:834`).
+- Pre-existing wording divergence between Kotlin's "Clear chat?"
+ and iOS's "Clear conversation?". Both platforms keep their
+ titles.
+- "Delete invitation" at `ChatListNavLink.swift:236` — has no
+ confirmation dialog (direct call to `deleteChat(chat)`); nothing
+ to modify.
+- Bolding the chat name. SwiftUI `Text + Text` supports `.bold()`
+ on the first term, but Jetpack Compose `AlertDialog` text is a
+ single unstyled string — keeping both unstyled preserves parity.
diff --git a/plans/delete-leave-dialog-with-profile.md b/plans/delete-leave-dialog-with-profile.md
new file mode 100644
index 0000000000..a05fb66532
--- /dev/null
+++ b/plans/delete-leave-dialog-with-profile.md
@@ -0,0 +1,249 @@
+# Show chat name in delete / leave / clear confirmation dialogs
+
+## Goal
+
+The current delete-contact, delete-group, delete-channel, leave-group,
+leave-channel and clear-chat confirmations are generic. From a long
+chat list, swiping on a row and triggering one of these actions opens
+a dialog whose title is "Delete group?", "Leave channel?", "Clear
+conversation?" — with no indication of *which* chat is the target. A
+user can easily act on the wrong chat.
+
+The fix: include the chat's display name in the dialog body, on a line
+of its own above the existing warning text. Nothing else changes —
+same title, same warning text, same buttons, same colors, same dialog
+shape. No profile picture, no layout changes, no new helpers, no new
+translation strings.
+
+We deliberately do NOT reuse the open-chat-link alert layout (centered
+profile image + name + open-chat button). That layout is the *invite*
+flow's identity; repurposing it for destructive confirmations would
+confuse the two flows visually. The minimum change that solves the
+"which chat?" problem is putting the name in the body text.
+
+## Why body, not title; why no new strings
+
+The title carries the action ("Delete group?", "Leave channel?"). The
+body carries the consequences ("Group will be deleted for all
+members…"). The chat name belongs with the body — it is the subject
+of the consequence, not part of the question.
+
+Adding the name to the title would require new format-string variants
+(`delete_group_named_question` etc.) and per-locale re-translation.
+Putting the name on its own line in the body is a pure code change —
+the existing translated warnings are concatenated with the chat name
+in code:
+
+```
+Tech Talk
+
+Group will be deleted for all members - this cannot be undone!
+```
+
+The display name appears first because the user wants to confirm
+*which* chat before reading *what* will happen. The blank line between
+the name and the warning makes the name visually distinct.
+
+## Current state
+
+### Multiplatform (Kotlin / Android / Desktop)
+
+All eight dialogs go through `AlertManager.shared.showAlertDialog` or
+`showAlertDialogButtonsColumn`:
+
+- `deleteGroupDialog` — `views/chat/group/GroupChatInfoView.kt:182`
+- `leaveGroupDialog` — `views/chat/group/GroupChatInfoView.kt:222`
+- `clearChatDialog` — `views/chat/ChatInfoView.kt:492`
+- `deleteContactOrConversationDialog` — `views/chat/ChatInfoView.kt:248`
+- `deleteActiveContactDialog` — `views/chat/ChatInfoView.kt:304`
+- `deleteContactWithoutConversation` — `views/chat/ChatInfoView.kt:361`
+- `deleteNotReadyContact` — `views/chat/ChatInfoView.kt:417`
+- `deleteContactConnectionAlert` — `views/chatlist/ChatListNavLinkView.kt:772`
+ (deletes a pending contact connection; takes a `PendingContactConnection`
+ whose `displayName` reflects any custom name the user set)
+
+Call sites (chat-info screens, chat-list swipe / overflow, contact
+list) funnel through these dispatcher functions.
+
+### iOS (Swift)
+
+Two SwiftUI patterns are used:
+
+- SwiftUI `Alert` with `primaryButton: .destructive` / `.cancel()`:
+ - `deleteGroupAlert` — `Views/ChatList/ChatListNavLink.swift:567`,
+ `Views/Chat/Group/GroupChatInfoView.swift:835`
+ - `leaveGroupAlert` — `Views/ChatList/ChatListNavLink.swift:622`,
+ `Views/Chat/Group/GroupChatInfoView.swift:872`
+ - `clearChatAlert` — `Views/ChatList/ChatListNavLink.swift:600`,
+ `Views/Chat/ChatInfoView.swift:577`,
+ `Views/Chat/Group/GroupChatInfoView.swift:858`
+- SwiftUI `ActionSheet`:
+ - `deleteContactOrConversationDialog` —
+ `Views/Chat/ChatInfoView.swift:1177`
+ - `deleteContactWithoutConversation` —
+ `Views/Chat/ChatInfoView.swift:1324`
+ - `deleteNotReadyContact` — `Views/Chat/ChatInfoView.swift:1348`
+
+`Alert(message:)` accepts `Text`, and `ActionSheet(message:)` (an
+existing optional parameter not used today) accepts `Text` too — so
+the name can be added by composing the existing message string with
+`"\n\n"` and the chat name. No widget changes.
+
+## Design
+
+| Dialog | Body today | Body after |
+|---|---|---|
+| Delete group | `Group will be deleted for all members – this cannot be undone!` | `Tech Talk` + blank line + existing text |
+| Delete channel | `Channel will be deleted for all subscribers – this cannot be undone!` | `SimpleX news` + blank line + existing text |
+| Leave group | `You will stop receiving messages from this group. …` | `Tech Talk` + blank line + existing text |
+| Clear chat | `All messages will be deleted – this cannot be undone! …` | `Alice` + blank line + existing text |
+| Delete contact (entry sheet) | *(no body today — title only + buttons)* | `Alice` (becomes the body) |
+| Delete contact (active variant) | `Contact will be deleted – this cannot be undone!` | `Alice` + blank line + existing text |
+| Confirm contact deletion (not-ready / no-conversation) | `Contact will be deleted – this cannot be undone!` | `Alice` + blank line + existing text |
+
+Title text is unchanged in every case. Existing titles
+(`delete_contact_question`, `confirm_delete_contact_question`, etc.)
+keep their semantic distinction — the "Confirm contact deletion?"
+title still appears for the not-ready / no-conversation paths.
+
+### Which name to use: `displayName`, not `chatViewName`
+
+The chat list row labels chats with `cInfo.chatViewName`
+(`ChatPreviewView.kt:87`), defined as:
+
+```kotlin
+val chatViewName: String
+ get() = localAlias.ifEmpty { displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") }
+```
+
+The dialog uses `chatInfo.displayName` (and `groupInfo.displayName`
+for the leave dialog). For most chats these are identical:
+
+- If `localAlias` is set, both resolve to the alias.
+- If `displayName == fullName` (or `fullName` is empty), both resolve
+ to `displayName`.
+
+For a contact with distinct display name and full name (no alias),
+the row would show `alice / Alice Smith` while the dialog shows
+`alice`. Acceptable: `displayName` is the recognizable identifier,
+shorter, and the dialog format (single line above the warning)
+benefits from concision. Two-part identifiers in the dialog would
+crowd the layout.
+
+### `clearNoteFolderDialog` is excluded
+
+The local notes folder is a single-instance object — there is only
+one per user — and its existing warning text already names it
+unambiguously. Adding the display name on its own line would be
+pure redundancy. Skipped.
+
+## Changes
+
+### Multiplatform (Kotlin)
+
+Each dispatcher function changes one argument: the `text =` parameter
+passed to `AlertManager.shared.showAlertDialog` /
+`showAlertDialogButtonsColumn`. The new value is the chat name + two
+newlines + the existing message text:
+
+```kotlin
+text = "${chatInfo.displayName}\n\n${generalGetString(messageId)}",
+parseHtml = false,
+```
+
+`parseHtml = false` is a new boolean parameter added to both alert
+helpers. It bypasses `escapedHtmlToAnnotatedString` so the
+user-controlled `displayName` is rendered as literal text, never
+interpreted as HTML markup (``, ``, `&`, etc.). The default
+remains `true`; only our delete-confirmation dispatchers opt out.
+
+For `leaveGroupDialog` the source is `groupInfo.displayName` (the
+function already takes `groupInfo` — no signature change needed,
+no caller updates needed).
+
+For `deleteGroupDialog`, also `groupInfo.displayName`, for consistency
+with `leaveGroupDialog` (both have `groupInfo` already in scope).
+
+For `deleteContactOrConversationDialog`, which has no `text =`
+parameter today, add `text = chatInfo.displayName` (no concatenation
+needed — the dialog had no body text before).
+
+### iOS
+
+Each of the eight call sites changes one argument: the `message:`
+parameter passed to `Alert(…)` or `ActionSheet(…)`. The new value
+composes the chat name with the existing localized message string:
+
+```swift
+message: Text("\(chat.chatInfo.displayName)\n\n\(existingMessage)"),
+```
+
+For the three `ActionSheet` sites that have no `message:` today, add
+`message: Text(chat.chatInfo.displayName)`.
+
+## Out of scope
+
+- Profile picture / avatar in any of these dialogs — excluded by
+ decision: the open-chat-link alert owns that layout, and reusing
+ it for destructive confirmations conflates two semantically
+ different flows.
+- The pre-existing wording divergence between Kotlin's
+ `clear_chat_question` ("Clear chat?") and iOS's "Clear
+ conversation?". Both platforms keep their existing titles.
+- Refactoring the iOS duplication between `ChatListNavLink` and
+ `GroupChatInfoView` / `ChatInfoView` (pre-existing `// TODO reuse
+ this and clearChatAlert with ChatInfoView` at
+ `GroupChatInfoView.swift:834`).
+- "Delete invitation" at `ChatListNavLink.swift:236` — goes through
+ `deleteChat(chat)` directly with no confirmation dialog. No dialog
+ to modify.
+- Bolding the chat name on its own line. SwiftUI `Text` concatenation
+ supports `.bold()`; Jetpack Compose `AlertDialog` text is a single
+ string. Keep both platforms unstyled for parity.
+
+## Verification
+
+Per platform, exercise every entry point and confirm the dialog body
+reads `` on its own line followed by a blank line followed
+by the existing warning:
+
+- Android & Desktop:
+ - Chat list swipe — direct contact, group, channel, business chat
+ → delete / clear / leave actions. (Note folder's clear dialog
+ is intentionally unchanged.)
+ - Chat info screens — "Delete contact" / "Delete group" / "Delete
+ channel" / "Clear conversation" / "Leave …" rows.
+ - Contact list (`ContactListNavView.kt:148`) — "Delete contact"
+ action.
+ - The multi-option contact-delete path: entry dialog (now has a
+ name body where it had none) → toggle dialog (name above the
+ warning) → success.
+- iOS:
+ - Same matrix from chat list swipe and chat info screens.
+ - Action-sheet contact-delete paths show the name as the
+ `message:` line.
+
+Edge cases:
+
+- Long chat name (40+ chars) — alert containers wrap automatically;
+ body now occupies 3+ lines (name on 2, blank line, warning on 1+).
+ Confirm via a chat renamed to a long string.
+- Special characters in name (emoji, RTL text, double quotes) —
+ render literally because the substitution is string concatenation,
+ not format expansion. A contact named `Bob "the builder"` displays
+ as `Bob "the builder"` on its own line. No quoting/escaping issue.
+- Empty `displayName` would render an empty first line above the
+ warning. In practice `displayName` is non-empty (the `NamedChat`
+ interface enforces it via `localAlias.ifEmpty { profile.displayName }`);
+ no defensive trimming added.
+
+Diff-level checks:
+
+- `git diff strings.xml` and `git diff '*Localizable.strings'` show
+ zero hunks. The change is pure code.
+- `git diff --stat` shows each platform touched in 2–4 files:
+ the dispatcher file(s) on Kotlin (`ChatInfoView.kt`,
+ `GroupChatInfoView.kt`), and the SwiftUI views holding the
+ alert/sheet builders on iOS.
+- Behavior is unchanged. Cancel returns to the prior screen;
+ confirm performs the same destructive API call as before.
diff --git a/scripts/flatpak/chat.simplex.simplex.metainfo.xml b/scripts/flatpak/chat.simplex.simplex.metainfo.xml
index b55a08df26..0d93315435 100644
--- a/scripts/flatpak/chat.simplex.simplex.metainfo.xml
+++ b/scripts/flatpak/chat.simplex.simplex.metainfo.xml
@@ -38,6 +38,50 @@
+