mobile: settings for auto-accepting images, link previews, spinner for link previews; privacy settings (#708)

* ios: settings for auto-accepting images, link previews, spinner for link previews

* android: settings for auto-accepting images, link previews, spinner for link previews, privacy settings

* update translation

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

* translation

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

* translation

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
This commit is contained in:
Evgeny Poberezkin
2022-05-30 08:59:04 +01:00
committed by GitHub
parent 7c1d573a17
commit 29e2c00811
28 changed files with 508 additions and 213 deletions
@@ -78,6 +78,8 @@ class AppPreferences(val context: Context) {
)
val performLA = mkBoolPreference(SHARED_PREFS_PERFORM_LA, false)
val laNoticeShown = mkBoolPreference(SHARED_PREFS_LA_NOTICE_SHOWN, false)
val privacyAcceptImages = mkBoolPreference(SHARED_PREFS_PRIVACY_ACCEPT_IMAGES, true)
val privacyLinkPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS, true)
private fun mkIntPreference(prefName: String, default: Int) =
Preference(
@@ -107,6 +109,8 @@ class AppPreferences(val context: Context) {
private const val SHARED_PREFS_WEBRTC_CALLS_ON_LOCK_SCREEN = "CallsOnLockScreen"
private const val SHARED_PREFS_PERFORM_LA = "PerformLA"
private const val SHARED_PREFS_LA_NOTICE_SHOWN = "LANoticeShown"
private const val SHARED_PREFS_PRIVACY_ACCEPT_IMAGES = "PrivacyAcceptImages"
private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews"
}
}
@@ -510,13 +514,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
val cItem = r.chatItem.chatItem
chatModel.addChatItem(cInfo, cItem)
val file = cItem.file
if (cItem.content.msgContent is MsgContent.MCImage && file != null && file.fileSize <= MAX_IMAGE_SIZE) {
withApi {
val chatItem = apiReceiveFile(file.fileId)
if (chatItem != null) {
chatItemSimpleUpdate(chatItem)
}
}
if (cItem.content.msgContent is MsgContent.MCImage && file != null && file.fileSize <= MAX_IMAGE_SIZE && appPrefs.privacyAcceptImages.get()) {
withApi { receiveFile(file.fileId) }
}
if (!cItem.isCall && (!isAppOnForeground(appContext) || chatModel.chatId.value != cInfo.id)) {
ntfManager.notifyMessageReceived(cInfo, cItem)
@@ -615,6 +614,13 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
}
}
suspend fun receiveFile(fileId: Long) {
val chatItem = apiReceiveFile(fileId)
if (chatItem != null) {
chatItemSimpleUpdate(chatItem)
}
}
private fun chatItemSimpleUpdate(aChatItem: AChatItem) {
val cInfo = aChatItem.chatInfo
val cItem = aChatItem.chatItem
@@ -26,7 +26,7 @@ import kotlinx.coroutines.launch
@Composable
fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
val composeState = remember { mutableStateOf(ComposeState()) }
val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }
BackHandler(onBack = close)
TerminalLayout(
chatModel.terminalItems,
@@ -35,7 +35,7 @@ fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
withApi {
// show "in progress"
chatModel.controller.sendCmd(CC.Console(composeState.value.message))
composeState.value = ComposeState()
composeState.value = ComposeState(useLinkPreviews = false)
// hide "in progress"
}
},
@@ -120,7 +120,7 @@ fun PreviewTerminalLayout() {
SimpleXTheme {
TerminalLayout(
terminalItems = TerminalItem.sampleData,
composeState = remember { mutableStateOf(ComposeState()) },
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) },
sendCommand = {},
close = {}
)
@@ -44,7 +44,8 @@ import kotlinx.datetime.Clock
fun ChatView(chatModel: ChatModel) {
val chat: Chat? = chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }
val user = chatModel.currentUser.value
val composeState = remember { mutableStateOf(ComposeState()) }
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = useLinkPreviews)) }
val attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) }
val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
@@ -83,6 +84,7 @@ fun ChatView(chatModel: ChatModel) {
scope,
attachmentBottomSheetState,
chatModel.chatItems,
useLinkPreviews = useLinkPreviews,
back = { chatModel.chatId.value = null },
info = { ModalManager.shared.showCustomModal { close -> ChatInfoView(chatModel, close) } },
openDirectChat = { contactId ->
@@ -104,14 +106,7 @@ fun ChatView(chatModel: ChatModel) {
}
},
receiveFile = { fileId ->
withApi {
val chatItem = chatModel.controller.apiReceiveFile(fileId)
if (chatItem != null) {
val cInfo = chatItem.chatInfo
val cItem = chatItem.chatItem
chatModel.upsertChatItem(cInfo, cItem)
}
}
withApi { chatModel.controller.receiveFile(fileId) }
},
startCall = { media ->
val cInfo = chat.chatInfo
@@ -143,6 +138,7 @@ fun ChatLayout(
scope: CoroutineScope,
attachmentBottomSheetState: ModalBottomSheetState,
chatItems: List<ChatItem>,
useLinkPreviews: Boolean,
back: () -> Unit,
info: () -> Unit,
openDirectChat: (Long) -> Unit,
@@ -175,7 +171,7 @@ fun ChatLayout(
modifier = Modifier.navigationBarsWithImePadding()
) { contentPadding ->
Box(Modifier.padding(contentPadding)) {
ChatItemsList(user, chat, composeState, chatItems, openDirectChat, deleteMessage, receiveFile, acceptCall)
ChatItemsList(user, chat, composeState, chatItems, useLinkPreviews, openDirectChat, deleteMessage, receiveFile, acceptCall)
}
}
}
@@ -261,6 +257,7 @@ fun ChatItemsList(
chat: Chat,
composeState: MutableState<ComposeState>,
chatItems: List<ChatItem>,
useLinkPreviews: Boolean,
openDirectChat: (Long) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit,
@@ -302,11 +299,11 @@ fun ChatItemsList(
} else {
Spacer(Modifier.size(42.dp))
}
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, showMember = showMember, deleteMessage = deleteMessage, receiveFile = receiveFile, acceptCall = acceptCall)
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, showMember = showMember, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, acceptCall = acceptCall)
}
} else {
Box(Modifier.padding(start = 86.dp, end = 12.dp)) {
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, deleteMessage = deleteMessage, receiveFile = receiveFile, acceptCall = acceptCall)
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, acceptCall = acceptCall)
}
}
} else { // direct message
@@ -317,7 +314,7 @@ fun ChatItemsList(
end = if (sent) 12.dp else 76.dp,
)
) {
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, deleteMessage = deleteMessage, receiveFile = receiveFile, acceptCall = acceptCall)
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, acceptCall = acceptCall)
}
}
}
@@ -375,12 +372,13 @@ fun PreviewChatLayout() {
chatItems = chatItems,
chatStats = Chat.ChatStats()
),
composeState = remember { mutableStateOf(ComposeState()) },
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
composeView = {},
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
scope = rememberCoroutineScope(),
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
chatItems = chatItems,
useLinkPreviews = true,
back = {},
info = {},
openDirectChat = {},
@@ -421,12 +419,13 @@ fun PreviewGroupChatLayout() {
chatItems = chatItems,
chatStats = Chat.ChatStats()
),
composeState = remember { mutableStateOf(ComposeState()) },
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
composeView = {},
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
scope = rememberCoroutineScope(),
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
chatItems = chatItems,
useLinkPreviews = true,
back = {},
info = {},
openDirectChat = {},
@@ -45,7 +45,7 @@ import java.io.File
sealed class ComposePreview {
object NoPreview: ComposePreview()
class CLinkPreview(val linkPreview: LinkPreview): ComposePreview()
class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview()
class ImagePreview(val image: String): ComposePreview()
class FilePreview(val fileName: String): ComposePreview()
}
@@ -60,12 +60,14 @@ data class ComposeState(
val message: String = "",
val preview: ComposePreview = ComposePreview.NoPreview,
val contextItem: ComposeContextItem = ComposeContextItem.NoContextItem,
val inProgress: Boolean = false
val inProgress: Boolean = false,
val useLinkPreviews: Boolean
) {
constructor(editingItem: ChatItem): this(
constructor(editingItem: ChatItem, useLinkPreviews: Boolean): this (
editingItem.content.text,
chatItemPreview(editingItem),
ComposeContextItem.EditingItem(editingItem)
ComposeContextItem.EditingItem(editingItem),
useLinkPreviews = useLinkPreviews
)
val editing: Boolean
@@ -88,7 +90,7 @@ data class ComposeState(
when (preview) {
is ComposePreview.ImagePreview -> false
is ComposePreview.FilePreview -> false
else -> true
else -> useLinkPreviews
}
val linkPreview: LinkPreview?
get() =
@@ -124,6 +126,7 @@ fun ComposeView(
val prevLinkUrl = remember { mutableStateOf<String?>(null) }
val pendingLinkUrl = remember { mutableStateOf<String?>(null) }
val cancelledLinks = remember { mutableSetOf<String>() }
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
val textStyle = remember { mutableStateOf(smallFont) }
// attachments
@@ -239,6 +242,7 @@ fun ComposeView(
fun loadLinkPreview(url: String, wait: Long? = null) {
if (pendingLinkUrl.value == url) {
composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(null))
withApi {
if (wait != null) delay(wait)
val lp = getLinkPreview(url)
@@ -277,7 +281,7 @@ fun ComposeView(
is ComposePreview.CLinkPreview -> {
val url = parseMessage(cs.message)
val lp = composePreview.linkPreview
if (url == lp.uri) {
if (lp != null && url == lp.uri) {
MsgContent.MCLink(cs.message, preview = lp)
} else {
MsgContent.MCText(cs.message)
@@ -299,7 +303,7 @@ fun ComposeView(
}
fun clearState() {
composeState.value = ComposeState()
composeState.value = ComposeState(useLinkPreviews = useLinkPreviews)
textStyle.value = smallFont
chosenImage.value = null
chosenFile.value = null
@@ -397,6 +401,7 @@ fun ComposeView(
if (uri != null) {
cancelledLinks.add(uri)
}
pendingLinkUrl.value = null
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
}
@@ -111,7 +111,7 @@ fun PreviewSendMsgView() {
val textStyle = remember { mutableStateOf(smallFont) }
SimpleXTheme {
SendMsgView(
composeState = remember { mutableStateOf(ComposeState()) },
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
sendMessage = {},
onMessageChange = { _ -> },
textStyle = textStyle
@@ -129,7 +129,7 @@ fun PreviewSendMsgView() {
fun PreviewSendMsgViewEditing() {
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
val textStyle = remember { mutableStateOf(smallFont) }
val composeStateEditing = ComposeState(editingItem = ChatItem.getSampleData())
val composeStateEditing = ComposeState(editingItem = ChatItem.getSampleData(), useLinkPreviews = true)
SimpleXTheme {
SendMsgView(
composeState = remember { mutableStateOf(composeStateEditing) },
@@ -150,7 +150,7 @@ fun PreviewSendMsgViewEditing() {
fun PreviewSendMsgViewInProgress() {
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
val textStyle = remember { mutableStateOf(smallFont) }
val composeStateInProgress = ComposeState(preview = ComposePreview.FilePreview("test.txt"), inProgress = true)
val composeStateInProgress = ComposeState(preview = ComposePreview.FilePreview("test.txt"), inProgress = true, useLinkPreviews = true)
SimpleXTheme {
SendMsgView(
composeState = remember { mutableStateOf(composeStateInProgress) },
@@ -26,7 +26,8 @@ import chat.simplex.app.views.helpers.*
fun CIImageView(
image: String,
file: CIFile?,
showMenu: MutableState<Boolean>
showMenu: MutableState<Boolean>,
receiveFile: (Long) -> Unit
) {
@Composable
fun loadingIndicator() {
@@ -98,11 +99,21 @@ fun CIImageView(
})
} else {
imageView(base64ToBitmap(image), onClick = {
if (file != null && file.fileStatus == CIFileStatus.RcvAccepted)
AlertManager.shared.showAlertMsg(
generalGetString(R.string.waiting_for_image),
generalGetString(R.string.image_will_be_received_when_contact_is_online)
)
if (file != null) {
when (file.fileStatus) {
CIFileStatus.RcvInvitation ->
receiveFile(file.fileId)
CIFileStatus.RcvAccepted ->
AlertManager.shared.showAlertMsg(
generalGetString(R.string.waiting_for_image),
generalGetString(R.string.image_will_be_received_when_contact_is_online)
)
CIFileStatus.RcvTransfer -> {} // ?
CIFileStatus.RcvComplete -> {} // ?
CIFileStatus.RcvCancelled -> {} // TODO
else -> {}
}
}
})
}
loadingIndicator()
@@ -36,6 +36,7 @@ fun ChatItemView(
cxt: Context,
uriHandler: UriHandler? = null,
showMember: Boolean = false,
useLinkPreviews: Boolean,
deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit,
acceptCall: (Contact) -> Unit
@@ -69,7 +70,7 @@ fun ChatItemView(
) {
ItemAction(stringResource(R.string.reply_verb), Icons.Outlined.Reply, onClick = {
if (composeState.value.editing) {
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem))
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
} else {
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
}
@@ -98,7 +99,7 @@ fun ChatItemView(
}
if (cItem.meta.editable) {
ItemAction(stringResource(R.string.edit_verb), Icons.Filled.Edit, onClick = {
composeState.value = ComposeState(editingItem = cItem)
composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews)
showMenu.value = false
})
}
@@ -203,7 +204,8 @@ fun PreviewChatItemView() {
ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
),
composeState = remember { mutableStateOf(ComposeState()) },
useLinkPreviews = true,
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
cxt = LocalContext.current,
deleteMessage = { _, _ -> },
receiveFile = {},
@@ -220,7 +222,8 @@ fun PreviewChatItemViewDeletedContent() {
User.sampleData,
ChatInfo.Direct.sampleData,
ChatItem.getDeletedContentSampleData(),
composeState = remember { mutableStateOf(ComposeState()) },
useLinkPreviews = true,
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
cxt = LocalContext.current,
deleteMessage = { _, _ -> },
receiveFile = {},
@@ -121,7 +121,7 @@ fun FramedItemView(
Column(Modifier.fillMaxWidth()) {
when (val mc = ci.content.msgContent) {
is MsgContent.MCImage -> {
CIImageView(image = mc.image, file = ci.file, showMenu)
CIImageView(image = mc.image, file = ci.file, showMenu, receiveFile)
if (mc.text == "") {
metaColor = Color.White
} else {
@@ -11,6 +11,7 @@ import androidx.compose.material.icons.outlined.Close
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
@@ -66,23 +67,36 @@ suspend fun getLinkPreview(url: String): LinkPreview? {
@Composable
fun ComposeLinkView(linkPreview: LinkPreview, cancelPreview: () -> Unit) {
fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit) {
Row(
Modifier.fillMaxWidth().padding(top = 8.dp).background(SentColorLight),
verticalAlignment = Alignment.CenterVertically
) {
val imageBitmap = base64ToBitmap(linkPreview.image).asImageBitmap()
Image(
imageBitmap,
stringResource(R.string.image_descr_link_preview),
modifier = Modifier.width(80.dp).height(60.dp).padding(end = 8.dp)
)
Column(Modifier.fillMaxWidth().weight(1F)) {
Text(linkPreview.title, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(
linkPreview.uri, maxLines = 1, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.body2
if (linkPreview == null) {
Box(
Modifier.fillMaxWidth().weight(1f).height(60.dp).padding(start = 16.dp),
contentAlignment = Alignment.CenterStart
) {
CircularProgressIndicator(
Modifier.size(16.dp),
color = HighOrLowlight,
strokeWidth = 2.dp
)
}
} else {
val imageBitmap = base64ToBitmap(linkPreview.image).asImageBitmap()
Image(
imageBitmap,
stringResource(R.string.image_descr_link_preview),
modifier = Modifier.width(80.dp).height(60.dp).padding(end = 8.dp)
)
Column(Modifier.fillMaxWidth().weight(1F)) {
Text(linkPreview.title, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(
linkPreview.uri, maxLines = 1, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.body2
)
}
}
IconButton(onClick = cancelPreview, modifier = Modifier.padding(0.dp)) {
Icon(
@@ -139,4 +153,12 @@ fun PreviewComposeLinkView() {
SimpleXTheme {
ComposeLinkView(LinkPreview.sampleData) { -> }
}
}
}
@Preview(showBackground = true)
@Composable
fun PreviewComposeLinkViewLoading() {
SimpleXTheme {
ComposeLinkView(null) { -> }
}
}
@@ -34,7 +34,7 @@ fun CallSettingsLayout(
) {
val lockCallState = remember { mutableStateOf(callOnLockScreen.get()) }
Text(
stringResource(R.string.call_settings),
stringResource(R.string.your_calls),
Modifier.padding(bottom = 24.dp),
style = MaterialTheme.typography.h1
)
@@ -71,6 +71,7 @@ fun SharedPreferenceToggle(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
),
modifier = Modifier.padding(end = 6.dp)
)
}
}
@@ -0,0 +1,65 @@
package chat.simplex.app.views.usersettings
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
@Composable
fun PrivacySettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
@Composable fun divider() = Divider(Modifier.padding(horizontal = 8.dp))
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
stringResource(R.string.your_privacy),
style = MaterialTheme.typography.h1,
modifier = Modifier.padding(start = 8.dp, bottom = 24.dp)
)
ChatLockSection(chatModel.performLA, setPerformLA)
Spacer(Modifier.height(24.dp))
AutoAcceptImagesSection(chatModel.controller.appPrefs.privacyAcceptImages)
divider()
LinkPreviewsSection(chatModel.controller.appPrefs.privacyLinkPreviews)
divider()
}
}
@Composable private fun AutoAcceptImagesSection(prefAcceptImages: Preference<Boolean>) {
SettingsSectionView() {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Outlined.Image,
contentDescription = stringResource(R.string.auto_accept_images),
tint = HighOrLowlight,
)
Spacer(Modifier.padding(horizontal = 4.dp))
SharedPreferenceToggle(stringResource(R.string.auto_accept_images), prefAcceptImages)
}
}
}
@Composable private fun LinkPreviewsSection(prefLinkPreviews: Preference<Boolean>) {
SettingsSectionView() {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Outlined.TravelExplore,
contentDescription = stringResource(R.string.send_link_previews),
tint = HighOrLowlight,
)
Spacer(Modifier.padding(horizontal = 4.dp))
SharedPreferenceToggle(stringResource(R.string.send_link_previews), prefLinkPreviews)
}
}
}
@@ -47,7 +47,6 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
profile = user.profile,
runServiceInBackground = chatModel.runServiceInBackground,
setRunServiceInBackground = ::setRunServiceInBackground,
performLA = chatModel.performLA,
setPerformLA = setPerformLA,
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
showCustomModal = { modalView -> { ModalManager.shared.showCustomModal { close -> modalView(chatModel, close) } } },
@@ -65,7 +64,6 @@ fun SettingsLayout(
profile: Profile,
runServiceInBackground: MutableState<Boolean>,
setRunServiceInBackground: (Boolean) -> Unit,
performLA: MutableState<Boolean>,
setPerformLA: (Boolean) -> Unit,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
@@ -103,7 +101,7 @@ fun SettingsLayout(
CallSettingsSection(showModal)
divider()
ChatLockSection(performLA, setPerformLA)
PrivacySettingsSection(showModal, setPerformLA)
divider()
PrivateNotificationsSection(runServiceInBackground, setRunServiceInBackground)
divider()
@@ -154,6 +152,18 @@ fun SettingsLayout(
}
}
@Composable private fun PrivacySettingsSection(showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), setPerformLA: (Boolean) -> Unit) {
SettingsSectionView(showModal { PrivacySettingsView(it, setPerformLA) }) {
Icon(
Icons.Outlined.Lock,
contentDescription = stringResource(R.string.privacy_and_security),
tint = HighOrLowlight,
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.privacy_and_security))
}
}
@Composable private fun HelpViewSection(showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) {
SettingsSectionView(showModal { HelpView(it) }) {
Icon(
@@ -258,13 +268,13 @@ fun SettingsLayout(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
),
modifier = Modifier.padding(end = 8.dp)
modifier = Modifier.padding(end = 6.dp)
)
}
}
}
@Composable private fun ChatLockSection(performLA: MutableState<Boolean>, setPerformLA: (Boolean) -> Unit) {
@Composable fun ChatLockSection(performLA: MutableState<Boolean>, setPerformLA: (Boolean) -> Unit) {
SettingsSectionView() {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
@@ -286,7 +296,7 @@ fun SettingsLayout(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
),
modifier = Modifier.padding(end = 8.dp)
modifier = Modifier.padding(end = 6.dp)
)
}
}
@@ -362,7 +372,6 @@ fun PreviewSettingsLayout() {
profile = Profile.sampleData,
runServiceInBackground = remember { mutableStateOf(true) },
setRunServiceInBackground = {},
performLA = remember { mutableStateOf(false) },
setPerformLA = {},
showModal = { {} },
showCustomModal = { {} },
@@ -168,7 +168,7 @@
<string name="toast_permission_denied">Разрешение не получено!</string>
<string name="use_camera_button">Камера</string>
<string name="from_gallery_button">Галерея</string>
<string name="choose_file">Файлы\n(v2.0)</string>
<string name="choose_file">Файлы</string>
<!-- help - ChatHelpView.kt -->
<string name="thank_you_for_installing_simplex">Спасибо, что установили <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
@@ -379,7 +379,8 @@
<string name="icon_descr_audio_call">аудиозвонок</string>
<!-- Call settings -->
<string name="call_settings">Настройки звонков</string>
<string name="call_settings">Аудио- и видеозвонки</string>
<string name="your_calls">Ваши звонки</string>
<string name="connect_calls_via_relay">Соединяться через сервер (relay)</string>
<string name="call_on_lock_screen">Звонки на экране блокировки:</string>
<string name="accept_call_on_lock_screen">Принимать</string>
@@ -421,4 +422,10 @@
<string name="integrity_msg_duplicate">повторное сообщение</string>
<string name="alert_title_skipped_messages">Пропущенные сообщения</string>
<string name="alert_text_skipped_messages_it_can_happen_when">Это может случится, когда:\n1. Сервер удалил сообщения, если они не были доставлены в течение 30 дней.\n2. Сервер, через который вы получаете сообщения от контакта, был обновлён и перезапущен.\n3. Соединение компроментировано.\nПожалуйста, соединитесь с девелоперами через Настройки, чтобы получать уведомления о серверах.\nМы планируем добавить избыточную доставку сообщений, чтобы не терять сообщения.</string>
<!-- Privacy settings -->
<string name="privacy_and_security">Конфиденциальность</string>
<string name="your_privacy">Конфиденциальность</string>
<string name="auto_accept_images">Автоприем изображений</string>
<string name="send_link_previews">Отправлять картинки ссылок</string>
</resources>
@@ -169,7 +169,7 @@
<string name="toast_permission_denied">Permission Denied!</string>
<string name="use_camera_button">Use Camera</string>
<string name="from_gallery_button">From Gallery</string>
<string name="choose_file">Choose file\n(new in v2.0)</string>
<string name="choose_file">Choose file</string>
<!-- help - ChatHelpView.kt -->
<string name="thank_you_for_installing_simplex">Thank you for installing <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
@@ -381,7 +381,8 @@
<string name="icon_descr_audio_call">audio call</string>
<!-- Call settings -->
<string name="call_settings">Call settings</string>
<string name="call_settings">Audio &amp; video calls</string>
<string name="your_calls">Your calls</string>
<string name="connect_calls_via_relay">Connect via relay</string>
<string name="call_on_lock_screen">Calls on lock screen:</string>
<string name="accept_call_on_lock_screen">Accept</string>
@@ -423,4 +424,10 @@
<string name="integrity_msg_duplicate">duplicate message</string>
<string name="alert_title_skipped_messages">Skipped messages</string>
<string name="alert_text_skipped_messages_it_can_happen_when">It can happen when:\n1. The messages expire on the server if they were not received for 30 days,\n2. The server you use to receive the messages from this contact was updated and restarted.\n3. The connection is compromised.\nPlease connect to the developers via Settings to receive the updates about the servers.\nWe will be adding server redundancy to prevent lost messages.</string>
<!-- Privacy settings -->
<string name="privacy_and_security">Privacy &amp; security</string>
<string name="your_privacy">Your privacy</string>
<string name="auto_accept_images">Auto-accept images</string>
<string name="send_link_previews">Send link previews</string>
</resources>
+2 -1
View File
@@ -578,7 +578,8 @@ func processReceivedMsg(_ res: ChatResponse) {
m.addChatItem(cInfo, cItem)
if case .image = cItem.content.msgContent,
let file = cItem.file,
file.fileSize <= maxImageSize {
file.fileSize <= maxImageSize,
UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES) {
Task {
await receiveFile(fileId: file.fileId)
}
@@ -10,7 +10,7 @@ import SwiftUI
enum ComposePreview {
case noPreview
case linkPreview(linkPreview: LinkPreview)
case linkPreview(linkPreview: LinkPreview?)
case imagePreview(imagePreview: String)
case filePreview(fileName: String)
}
@@ -26,6 +26,7 @@ struct ComposeState {
var preview: ComposePreview
var contextItem: ComposeContextItem
var inProgress: Bool = false
var useLinkPreviews: Bool = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS)
init(
message: String = "",
@@ -80,7 +81,7 @@ struct ComposeState {
case .filePreview:
return false
default:
return true
return useLinkPreviews
}
}
@@ -406,11 +407,13 @@ struct ComposeView: View {
if let uri = composeState.linkPreview()?.uri.absoluteString {
cancelledLinks.insert(uri)
}
pendingLinkUrl = nil
composeState = composeState.copy(preview: .noPreview)
}
private func loadLinkPreview(_ url: URL) {
if pendingLinkUrl == url {
composeState = composeState.copy(preview: .linkPreview(linkPreview: nil))
getLinkPreview(url: url) { linkPreview in
if let linkPreview = linkPreview,
pendingLinkUrl == url {
@@ -432,6 +435,7 @@ struct ComposeView: View {
switch (composeState.preview) {
case let .linkPreview(linkPreview: linkPreview):
if let url = parseMessage(composeState.message),
let linkPreview = linkPreview,
url == linkPreview.uri {
return .link(text: composeState.message, preview: linkPreview)
} else {
@@ -47,11 +47,23 @@ struct CIImageView: View {
let uiImage = UIImage(data: data) {
imageView(uiImage)
.onTapGesture {
if case .rcvAccepted = file?.fileStatus {
AlertManager.shared.showAlertMsg(
title: "Waiting for image",
message: "Image will be received when your contact is online, please wait or check later!"
)
if let file = file {
switch file.fileStatus {
case .rcvInvitation:
Task {
await receiveFile(fileId: file.fileId)
// TODO image accepted alert?
}
case .rcvAccepted:
AlertManager.shared.showAlertMsg(
title: "Waiting for image",
message: "Image will be received when your contact is online, please wait or check later!"
)
case .rcvTransfer: () // ?
case .rcvComplete: () // ?
case .rcvCancelled: () // TODO
default: ()
}
}
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,125 @@
//
// PrivacySettings.swift
// SimpleX (iOS)
//
// Created by Evgeny on 29/05/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
struct PrivacySettings: View {
@AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true
@AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true
var body: some View {
VStack {
List {
Section("Device") {
SimplexLockSetting()
}
Section("Chats") {
settingsRow("photo") {
Toggle("Auto-accept images", isOn: $autoAcceptImages)
}
settingsRow("network") {
Toggle("Send link previews", isOn: $useLinkPreviews)
}
}
}
}
}
}
struct SimplexLockSetting: View {
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@State var performLA: Bool = UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)
@State private var performLAToggleReset = false
@State var laAlert: laSettingViewAlert? = nil
enum laSettingViewAlert: Identifiable {
case laTurnedOnAlert
case laFailedAlert
case laUnavailableInstructionAlert
case laUnavailableTurningOffAlert
var id: laSettingViewAlert { get { self } }
}
var body: some View {
settingsRow("lock") {
Toggle("SimpleX Lock", isOn: $performLA)
}
.onChange(of: performLA) { performLAToggle in
prefLANoticeShown = true
if performLAToggleReset {
performLAToggleReset = false
} else {
if performLAToggle {
enableLA()
} else {
disableLA()
}
}
}
.alert(item: $laAlert) { alertItem in
switch alertItem {
case .laTurnedOnAlert: return laTurnedOnAlert()
case .laFailedAlert: return laFailedAlert()
case .laUnavailableInstructionAlert: return laUnavailableInstructionAlert()
case .laUnavailableTurningOffAlert: return laUnavailableTurningOffAlert()
}
}
}
private func enableLA() {
authenticate(reason: NSLocalizedString("Enable SimpleX Lock", comment: "authentication reason")) { laResult in
switch laResult {
case .success:
prefPerformLA = true
laAlert = .laTurnedOnAlert
case .failed:
prefPerformLA = false
withAnimation() {
performLA = false
}
performLAToggleReset = true
laAlert = .laFailedAlert
case .unavailable:
prefPerformLA = false
withAnimation() {
performLA = false
}
performLAToggleReset = true
laAlert = .laUnavailableInstructionAlert
}
}
}
private func disableLA() {
authenticate(reason: NSLocalizedString("Disable SimpleX Lock", comment: "authentication reason")) { laResult in
switch (laResult) {
case .success:
prefPerformLA = false
case .failed:
prefPerformLA = true
withAnimation() {
performLA = true
}
performLAToggleReset = true
laAlert = .laFailedAlert
case .unavailable:
prefPerformLA = false
laAlert = .laUnavailableTurningOffAlert
}
}
}
}
struct PrivacySettings_Previews: PreviewProvider {
static var previews: some View {
PrivacySettings()
}
}
@@ -11,14 +11,13 @@ import SwiftUI
struct SettingsButton: View {
@EnvironmentObject var chatModel: ChatModel
@State private var showSettings = false
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
var body: some View {
Button { showSettings = true } label: {
Image(systemName: "gearshape")
}
.sheet(isPresented: $showSettings, content: {
SettingsView(showSettings: $showSettings, performLA: prefPerformLA)
SettingsView(showSettings: $showSettings)
})
}
}
@@ -20,6 +20,8 @@ let DEFAULT_PERFORM_LA = "performLocalAuthentication"
let DEFAULT_USE_NOTIFICATIONS = "useNotifications"
let DEFAULT_PENDING_CONNECTIONS = "pendingConnections"
let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay"
let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews"
let appDefaults: [String:Any] = [
DEFAULT_SHOW_LA_NOTICE: false,
@@ -27,7 +29,9 @@ let appDefaults: [String:Any] = [
DEFAULT_PERFORM_LA: false,
DEFAULT_USE_NOTIFICATIONS: false,
DEFAULT_PENDING_CONNECTIONS: true,
DEFAULT_WEBRTC_POLICY_RELAY: true
DEFAULT_WEBRTC_POLICY_RELAY: true,
DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
DEFAULT_PRIVACY_LINK_PREVIEWS: true
]
private var indent: CGFloat = 36
@@ -36,24 +40,10 @@ struct SettingsView: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var chatModel: ChatModel
@Binding var showSettings: Bool
@State var performLA: Bool = false
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@AppStorage(DEFAULT_USE_NOTIFICATIONS) private var useNotifications = false
@AppStorage(DEFAULT_PENDING_CONNECTIONS) private var pendingConnections = true
@State private var performLAToggleReset = false
@State var showNotificationsAlert: Bool = false
@State var whichNotificationsAlert = NotificationAlert.enable
@State var alert: SettingsViewAlert? = nil
enum SettingsViewAlert: Identifiable {
case laTurnedOnAlert
case laFailedAlert
case laUnavailableInstructionAlert
case laUnavailableTurningOffAlert
var id: SettingsViewAlert { get { self } }
}
var body: some View {
let user: User = chatModel.currentUser!
@@ -79,12 +69,15 @@ struct SettingsView: View {
Section("Settings") {
NavigationLink {
CallSettings()
.navigationTitle("Call settings")
.navigationTitle("Your calls")
} label: {
settingsRow("video") { Text("Call settings") }
settingsRow("video") { Text("Audio & video calls") }
}
settingsRow("lock") {
Toggle("SimpleX Lock", isOn: $performLA)
NavigationLink {
PrivacySettings()
.navigationTitle("Your privacy")
} label: {
settingsRow("lock") { Text("Privacy & security") }
}
settingsRow("link") {
Toggle("Show pending connections", isOn: $pendingConnections)
@@ -156,76 +149,6 @@ struct SettingsView: View {
}
}
.navigationTitle("Your settings")
.onChange(of: performLA) { performLAToggle in
prefLANoticeShown = true
if performLAToggleReset {
performLAToggleReset = false
} else {
if performLAToggle {
enableLA()
} else {
disableLA()
}
}
}
.alert(item: $alert) { alertItem in
switch alertItem {
case .laTurnedOnAlert: return laTurnedOnAlert()
case .laFailedAlert: return laFailedAlert()
case .laUnavailableInstructionAlert: return laUnavailableInstructionAlert()
case .laUnavailableTurningOffAlert: return laUnavailableTurningOffAlert()
}
}
}
}
private func enableLA() {
authenticate(reason: NSLocalizedString("Enable SimpleX Lock", comment: "authentication reason")) { laResult in
switch laResult {
case .success:
prefPerformLA = true
alert = .laTurnedOnAlert
case .failed:
prefPerformLA = false
withAnimation() {
performLA = false
}
performLAToggleReset = true
alert = .laFailedAlert
case .unavailable:
prefPerformLA = false
withAnimation() {
performLA = false
}
performLAToggleReset = true
alert = .laUnavailableInstructionAlert
}
}
}
private func disableLA() {
authenticate(reason: NSLocalizedString("Disable SimpleX Lock", comment: "authentication reason")) { laResult in
switch (laResult) {
case .success:
prefPerformLA = false
case .failed:
prefPerformLA = true
withAnimation() {
performLA = true
}
performLAToggleReset = true
alert = .laFailedAlert
case .unavailable:
prefPerformLA = false
alert = .laUnavailableTurningOffAlert
}
}
}
private func settingsRow<Content : View>(_ icon: String, content: @escaping () -> Content) -> some View {
ZStack(alignment: .leading) {
Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(.secondary)
content().padding(.leading, indent)
}
}
@@ -326,6 +249,13 @@ struct SettingsView: View {
}
}
func settingsRow<Content : View>(_ icon: String, content: @escaping () -> Content) -> some View {
ZStack(alignment: .leading) {
Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(.secondary)
content().padding(.leading, indent)
}
}
struct ProfilePreview: View {
var profileOf: NamedChat
var color = Color(uiColor: .tertiarySystemGroupedBackground)
@@ -176,6 +176,11 @@
<target>Attach</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Audio &amp; video calls" xml:space="preserve">
<source>Audio &amp; video calls</source>
<target>Audio &amp; video calls</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Authentication failed" xml:space="preserve">
<source>Authentication failed</source>
<target>Authentication failed</target>
@@ -186,16 +191,16 @@
<target>Authentication unavailable</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept images" xml:space="preserve">
<source>Auto-accept images</source>
<target>Auto-accept images</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Call already ended!" xml:space="preserve">
<source>Call already ended!</source>
<target>Call already ended!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Call settings" xml:space="preserve">
<source>Call settings</source>
<target>Call settings</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Cancel" xml:space="preserve">
<source>Cancel</source>
<target>Cancel</target>
@@ -227,8 +232,8 @@
<note>notification</note>
</trans-unit>
<trans-unit id="Choose file" xml:space="preserve">
<source>Choose file (new in v2.0)</source>
<target>Choose file (new in v2.0)</target>
<source>Choose file</source>
<target>Choose file</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Choose from library" xml:space="preserve">
@@ -456,6 +461,11 @@
<target>Develop</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Device" xml:space="preserve">
<source>Device</source>
<target>Device</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Device authentication is disabled. Turning off SimpleX Lock." xml:space="preserve">
<source>Device authentication is disabled. Turning off SimpleX Lock.</source>
<target>Device authentication is disabled. Turning off SimpleX Lock.</target>
@@ -776,6 +786,11 @@ We will be adding server redundancy to prevent lost messages.</target>
<target>Pre-arrange the calls, as notifications arrive with a delay (we are improving it).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Privacy &amp; security" xml:space="preserve">
<source>Privacy &amp; security</source>
<target>Privacy &amp; security</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Privacy redefined" xml:space="preserve">
<source>Privacy redefined</source>
<target>Privacy redefined</target>
@@ -856,6 +871,11 @@ We will be adding server redundancy to prevent lost messages.</target>
<target>Scan contact's QR code</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send link previews" xml:space="preserve">
<source>Send link previews</source>
<target>Send link previews</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server connected" xml:space="preserve">
<source>Server connected</source>
<target>Server connected</target>
@@ -1137,6 +1157,11 @@ To connect, please ask your contact to create another connection link and check
<target>Your SimpleX contact address</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your calls" xml:space="preserve">
<source>Your calls</source>
<target>Your calls</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat address" xml:space="preserve">
<source>Your chat address</source>
<target>Your chat address</target>
@@ -1174,6 +1199,11 @@ You can cancel this connection and remove the contact (and try later with a new
<target>Your contact sent a file that is larger than currently supported maximum size (%@).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your privacy" xml:space="preserve">
<source>Your privacy</source>
<target>Your privacy</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile is stored on your device and shared only with your contacts.&#10;SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts.
SimpleX servers cannot see your profile.</source>
@@ -1,6 +1,3 @@
/* No comment provided by engineer. */
"Choose file" = "Choose file (new in v2.0)";
/* No comment provided by engineer. */
"Connecting server…" = "Connecting to server…";
@@ -176,6 +176,11 @@
<target>Прикрепить</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Audio &amp; video calls" xml:space="preserve">
<source>Audio &amp; video calls</source>
<target>Аудио- и видеозвонки</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Authentication failed" xml:space="preserve">
<source>Authentication failed</source>
<target>Ошибка аутентификации</target>
@@ -186,16 +191,16 @@
<target>Аутентификация недоступна</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept images" xml:space="preserve">
<source>Auto-accept images</source>
<target>Автоприем изображений</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Call already ended!" xml:space="preserve">
<source>Call already ended!</source>
<target>Звонок уже завершен!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Call settings" xml:space="preserve">
<source>Call settings</source>
<target>Настройки звонков</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Cancel" xml:space="preserve">
<source>Cancel</source>
<target>Отменить</target>
@@ -213,7 +218,7 @@
</trans-unit>
<trans-unit id="Chats" xml:space="preserve">
<source>Chats</source>
<target>Назад</target>
<target>Чаты</target>
<note>back button to return to chats list</note>
</trans-unit>
<trans-unit id="Check messages" xml:space="preserve">
@@ -227,8 +232,8 @@
<note>notification</note>
</trans-unit>
<trans-unit id="Choose file" xml:space="preserve">
<source>Choose file (new in v2.0)</source>
<target>Выбрать файл (v2.0)</target>
<source>Choose file</source>
<target>Выбрать файл</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Choose from library" xml:space="preserve">
@@ -456,6 +461,11 @@
<target>Для разработчиков</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Device" xml:space="preserve">
<source>Device</source>
<target>Устройство</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Device authentication is disabled. Turning off SimpleX Lock." xml:space="preserve">
<source>Device authentication is disabled. Turning off SimpleX Lock.</source>
<target>Аутентификация устройства выключена. Отключение блокировки SimpleX Chat.</target>
@@ -776,6 +786,11 @@ We will be adding server redundancy to prevent lost messages.</source>
<target>Назначайте звонки заранее, поскольку уведомления приходят с задержкой (мы улучшаем это)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Privacy &amp; security" xml:space="preserve">
<source>Privacy &amp; security</source>
<target>Конфиденциальность</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Privacy redefined" xml:space="preserve">
<source>Privacy redefined</source>
<target>Более конфиденциальный</target>
@@ -856,6 +871,11 @@ We will be adding server redundancy to prevent lost messages.</source>
<target>Сосканировать QR код контакта</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send link previews" xml:space="preserve">
<source>Send link previews</source>
<target>Отправлять картинки ссылок</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server connected" xml:space="preserve">
<source>Server connected</source>
<target>Установлено соединение с сервером</target>
@@ -1137,6 +1157,11 @@ To connect, please ask your contact to create another connection link and check
<target>Ваш SimpleX адрес</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your calls" xml:space="preserve">
<source>Your calls</source>
<target>Ваши звонки</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat address" xml:space="preserve">
<source>Your chat address</source>
<target>Ваш SimpleX адрес</target>
@@ -1174,6 +1199,11 @@ You can cancel this connection and remove the contact (and try later with a new
<target>Ваш контакт отправил файл, размер которого превышает максимальный размер (%@).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your privacy" xml:space="preserve">
<source>Your privacy</source>
<target>Конфиденциальность</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile is stored on your device and shared only with your contacts.&#10;SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts.
SimpleX servers cannot see your profile.</source>
@@ -1,6 +1,3 @@
/* No comment provided by engineer. */
"Choose file" = "Choose file (new in v2.0)";
/* No comment provided by engineer. */
"Connecting server…" = "Connecting to server…";
@@ -29,6 +29,7 @@
5C3A88CE27DF50170060F1C2 /* DetermineWidth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */; };
5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3A88D027DF57800060F1C2 /* FramedItemView.swift */; };
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */; };
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */; };
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; };
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; };
5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; };
@@ -156,6 +157,7 @@
5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineWidth.swift; sourceTree = "<group>"; };
5C3A88D027DF57800060F1C2 /* FramedItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FramedItemView.swift; sourceTree = "<group>"; };
5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrityErrorItemView.swift; sourceTree = "<group>"; };
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = "<group>"; };
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = "<group>"; };
5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = "<group>"; };
5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = "<group>"; };
@@ -468,6 +470,7 @@
5CB924D327A853F100ACCCDD /* SettingsButton.swift */,
5CB924D627A8563F00ACCCDD /* SettingsView.swift */,
5C05DF522840AA1D00C683F9 /* CallSettings.swift */,
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */,
5CB924E327A8683A00ACCCDD /* UserAddress.swift */,
5CB924E027A867BA00ACCCDD /* UserProfile.swift */,
5C577F7C27C83AA10006112D /* MarkdownHelp.swift */,
@@ -684,6 +687,7 @@
buildActionMask = 2147483647;
files = (
5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */,
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */,
5CDCAD7F281894FB00503DA2 /* API.swift in Sources */,
5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */,
5CDCAD81281A7E2700503DA2 /* Notifications.swift in Sources */,
-3
View File
@@ -1,6 +1,3 @@
/* No comment provided by engineer. */
"Choose file" = "Choose file (new in v2.0)";
/* No comment provided by engineer. */
"Connecting server…" = "Connecting to server…";
+23 -5
View File
@@ -119,6 +119,9 @@
/* No comment provided by engineer. */
"Attach" = "Прикрепить";
/* No comment provided by engineer. */
"Audio & video calls" = "Аудио- и видеозвонки";
/* No comment provided by engineer. */
"audio call (not e2e encrypted)" = "аудиозвонок (не e2e зашифрованный)";
@@ -128,6 +131,9 @@
/* No comment provided by engineer. */
"Authentication unavailable" = "Аутентификация недоступна";
/* No comment provided by engineer. */
"Auto-accept images" = "Автоприем изображений";
/* integrity error chat item */
"bad message hash" = "ошибка хэш сообщения";
@@ -146,9 +152,6 @@
/* call status */
"call in progress" = "активный звонок";
/* No comment provided by engineer. */
"Call settings" = "Настройки звонков";
/* call status */
"calling…" = "входящий звонок…";
@@ -162,7 +165,7 @@
"Chat with the developers" = "Соединиться с разработчиками";
/* back button to return to chats list */
"Chats" = "Назад";
"Chats" = "Чаты";
/* No comment provided by engineer. */
"Check messages" = "Проверять сообщения";
@@ -171,7 +174,7 @@
"Checking new messages..." = "Проверяются новые сообщения...";
/* No comment provided by engineer. */
"Choose file" = "Выбрать файл (v2.0)";
"Choose file" = "Выбрать файл";
/* No comment provided by engineer. */
"Choose from library" = "Выбрать из библиотеки";
@@ -338,6 +341,9 @@
/* No comment provided by engineer. */
"Develop" = "Для разработчиков";
/* No comment provided by engineer. */
"Device" = "Устройство";
/* No comment provided by engineer. */
"Device authentication is disabled. Turning off SimpleX Lock." = "Аутентификация устройства выключена. Отключение блокировки SimpleX Chat.";
@@ -554,6 +560,9 @@
/* No comment provided by engineer. */
"Pre-arrange the calls, as notifications arrive with a delay (we are improving it)." = "Назначайте звонки заранее, поскольку уведомления приходят с задержкой (мы улучшаем это)";
/* No comment provided by engineer. */
"Privacy & security" = "Конфиденциальность";
/* No comment provided by engineer. */
"Privacy redefined" = "Более конфиденциальный";
@@ -611,6 +620,9 @@
/* No comment provided by engineer. */
"secret" = "секрет";
/* No comment provided by engineer. */
"Send link previews" = "Отправлять картинки ссылок";
/* No comment provided by engineer. */
"Server connected" = "Установлено соединение с сервером";
@@ -809,6 +821,9 @@
/* No comment provided by engineer. */
"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Вы будете аутентифицированы при запуске и возобновлении приложения, которое было 30 секунд в фоновом режиме.";
/* No comment provided by engineer. */
"Your calls" = "Ваши звонки";
/* No comment provided by engineer. */
"Your chat address" = "Ваш SimpleX адрес";
@@ -830,6 +845,9 @@
/* No comment provided by engineer. */
"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ваш контакт отправил файл, размер которого превышает максимальный размер (%@).";
/* No comment provided by engineer. */
"Your privacy" = "Конфиденциальность";
/* No comment provided by engineer. */
"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Ваш профиль хранится на вашем устройстве и отправляется только вашим контактам.\nSimpleX серверы не могут получить доступ к вашему профилю.";