Merge branch 'master' into sqlcipher

This commit is contained in:
Evgeny Poberezkin 2022-09-01 17:46:56 +01:00
commit 6597400f61
36 changed files with 574 additions and 128 deletions

View file

@ -79,18 +79,14 @@ You can use SimpleX with your own servers and still communicate with people usin
## News and updates ## News and updates
Selected updates: Recent updates:
[Aug 8, 2022. v3.1: secret chat groups, access via Tor, reduced battery and traffic usage, advanced netwrok settings, etc.](./blog/20220808-simplex-chat-v3.1-chat-groups.md) [Sep 1, 2022. v3.2: incognito mode, support .onion server hostnames, setting contact names, changing color scheme, etc. Implementation audit is arranged for October!](./blog/20220901-simplex-chat-v3.2-incognito-mode.md)
[Aug 8, 2022. v3.1: secret chat groups, access via Tor, reduced battery and traffic usage, advanced network settings, etc.](./blog/20220808-simplex-chat-v3.1-chat-groups.md)
[Jul 11, 2022. v3.0: instant push notifications for iOS, e2e encrypted WebRTC audio/video calls, chat database export/import, privacy and performance improvements](./blog/20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md) [Jul 11, 2022. v3.0: instant push notifications for iOS, e2e encrypted WebRTC audio/video calls, chat database export/import, privacy and performance improvements](./blog/20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md)
[May 11, 2022. v2.0 released - sending images and files in mobile apps](./blog/20220511-simplex-chat-v2-images-files.md)
[Mar 08, 2022 Mobile apps for iOS and Android released](./blog/20220308-simplex-chat-mobile-apps.md)
[Jan 12, 2022. SimpleX v1 released: the only messaging and application platform without user identities](./20220112-simplex-chat-v1-released.md)
[All updates](./blog) [All updates](./blog)
## Make a private connection ## Make a private connection
@ -177,10 +173,14 @@ If you are considering developing with SimpleX platform please get in touch for
- ✅ Chat database export and import - ✅ Chat database export and import
- ✅ Chat groups in mobile apps. - ✅ Chat groups in mobile apps.
- ✅ Connecting to messaging servers via Tor. - ✅ Connecting to messaging servers via Tor.
- 🏗 Dual server addresses to access messaging servers as v3 hidden services (in progress). - ✅ Dual server addresses to access messaging servers as v3 hidden services.
- 🏗 Chat server and TypeScript client SDK to develop chat interfaces, integrations and chat bots (in progress). - ✅ Chat server and TypeScript client SDK to develop chat interfaces, integrations and chat bots (ready for announcement).
- Chat database encryption. - ✅ Incognito mode to share a new random name with each contact.
- 🏗 Chat database encryption.
- 🏗 Links to join groups and improve groups stability.
- Disappearing messages, with mutual agreement. - Disappearing messages, with mutual agreement.
- Voice messages
- Video messages
- Web widgets for custom interactivity in the chats. - Web widgets for custom interactivity in the chats.
- SMP protocol improvements: - SMP protocol improvements:
- SMP queue redundancy and rotation. - SMP queue redundancy and rotation.
@ -191,6 +191,8 @@ If you are considering developing with SimpleX platform please get in touch for
- the server doesn't have information about your contacts and groups. - the server doesn't have information about your contacts and groups.
- Channels server for large groups and broadcast channels. - Channels server for large groups and broadcast channels.
- Media server to optimize sending large files to groups. - Media server to optimize sending large files to groups.
- Desktop client.
- Using the same profile on multiple devices.
## Help us pay for 3rd party security audit ## Help us pay for 3rd party security audit

View file

@ -11,8 +11,8 @@ android {
applicationId "chat.simplex.app" applicationId "chat.simplex.app"
minSdk 29 minSdk 29
targetSdk 32 targetSdk 32
versionCode 49 versionCode 52
versionName "3.2" versionName "3.2.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk { ndk {

View file

@ -15,6 +15,7 @@ import androidx.compose.material.Surface
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Replay import androidx.compose.material.icons.outlined.Replay
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -51,7 +52,11 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
ProcessLifecycleOwner.get().lifecycle.addObserver(this) ProcessLifecycleOwner.get().lifecycle.addObserver(this)
// testJson() // testJson()
val m = vm.chatModel val m = vm.chatModel
processNotificationIntent(intent, m) // When call ended and orientation changes, it re-process old intent, it's unneeded.
// Only needed to be processed on first creation of activity
if (savedInstanceState == null) {
processNotificationIntent(intent, m)
}
setContent { setContent {
SimpleXTheme { SimpleXTheme {
Surface( Surface(
@ -223,7 +228,7 @@ fun MainPage(
showLANotice: () -> Unit showLANotice: () -> Unit
) { ) {
// this with LaunchedEffect(userAuthorized.value) fixes bottom sheet visibly collapsing after authentication // this with LaunchedEffect(userAuthorized.value) fixes bottom sheet visibly collapsing after authentication
var chatsAccessAuthorized by remember { mutableStateOf(false) } var chatsAccessAuthorized by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(userAuthorized.value) { LaunchedEffect(userAuthorized.value) {
if (chatModel.controller.appPrefs.performLA.get()) { if (chatModel.controller.appPrefs.performLA.get()) {
delay(500L) delay(500L)

View file

@ -67,6 +67,7 @@ class ChatModel(val controller: ChatController) {
fun hasChat(id: String): Boolean = chats.firstOrNull { it.id == id } != null fun hasChat(id: String): Boolean = chats.firstOrNull { it.id == id } != null
fun getChat(id: String): Chat? = chats.firstOrNull { it.id == id } fun getChat(id: String): Chat? = chats.firstOrNull { it.id == id }
fun getContactChat(contactId: Long): Chat? = chats.firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId }
private fun getChatIndex(id: String): Int = chats.indexOfFirst { it.id == id } private fun getChatIndex(id: String): Int = chats.indexOfFirst { it.id == id }
fun addChat(chat: Chat) = chats.add(index = 0, chat) fun addChat(chat: Chat) = chats.add(index = 0, chat)

View file

@ -88,6 +88,8 @@ class AppPreferences(val context: Context) {
val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null) val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null)
val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false) val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false)
val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false) val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false)
val networkHostMode = mkStrPreference(SHARED_PREFS_NETWORK_HOST_MODE, HostMode.OnionViaSocks.name)
val networkRequiredHostMode = mkBoolPreference(SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE, false)
val networkTCPConnectTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT, NetCfg.defaults.tcpConnectTimeout, NetCfg.proxyDefaults.tcpConnectTimeout) val networkTCPConnectTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT, NetCfg.defaults.tcpConnectTimeout, NetCfg.proxyDefaults.tcpConnectTimeout)
val networkTCPTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT, NetCfg.defaults.tcpTimeout, NetCfg.proxyDefaults.tcpTimeout) val networkTCPTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT, NetCfg.defaults.tcpTimeout, NetCfg.proxyDefaults.tcpTimeout)
val networkSMPPingInterval = mkLongPreference(SHARED_PREFS_NETWORK_SMP_PING_INTERVAL, NetCfg.defaults.smpPingInterval) val networkSMPPingInterval = mkLongPreference(SHARED_PREFS_NETWORK_SMP_PING_INTERVAL, NetCfg.defaults.smpPingInterval)
@ -159,6 +161,8 @@ class AppPreferences(val context: Context) {
private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart" private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart"
private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools" private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools"
private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy" private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy"
private const val SHARED_PREFS_NETWORK_HOST_MODE = "NetworkHostMode"
private const val SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE = "NetworkRequiredHostMode"
private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT = "NetworkTCPConnectTimeout" private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT = "NetworkTCPConnectTimeout"
private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT = "NetworkTCPTimeout" private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT = "NetworkTCPTimeout"
private const val SHARED_PREFS_NETWORK_SMP_PING_INTERVAL = "NetworkSMPPingInterval" private const val SHARED_PREFS_NETWORK_SMP_PING_INTERVAL = "NetworkSMPPingInterval"
@ -1103,6 +1107,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
fun getNetCfg(): NetCfg { fun getNetCfg(): NetCfg {
val useSocksProxy = appPrefs.networkUseSocksProxy.get() val useSocksProxy = appPrefs.networkUseSocksProxy.get()
val socksProxy = if (useSocksProxy) ":9050" else null val socksProxy = if (useSocksProxy) ":9050" else null
val hostMode = HostMode.valueOf(appPrefs.networkHostMode.get()!!)
val requiredHostMode = appPrefs.networkRequiredHostMode.get()
val tcpConnectTimeout = appPrefs.networkTCPConnectTimeout.get() val tcpConnectTimeout = appPrefs.networkTCPConnectTimeout.get()
val tcpTimeout = appPrefs.networkTCPTimeout.get() val tcpTimeout = appPrefs.networkTCPTimeout.get()
val smpPingInterval = appPrefs.networkSMPPingInterval.get() val smpPingInterval = appPrefs.networkSMPPingInterval.get()
@ -1117,6 +1123,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
} }
return NetCfg( return NetCfg(
socksProxy = socksProxy, socksProxy = socksProxy,
hostMode = hostMode,
requiredHostMode = requiredHostMode,
tcpConnectTimeout = tcpConnectTimeout, tcpConnectTimeout = tcpConnectTimeout,
tcpTimeout = tcpTimeout, tcpTimeout = tcpTimeout,
tcpKeepAlive = tcpKeepAlive, tcpKeepAlive = tcpKeepAlive,
@ -1126,6 +1134,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
fun setNetCfg(cfg: NetCfg) { fun setNetCfg(cfg: NetCfg) {
appPrefs.networkUseSocksProxy.set(cfg.useSocksProxy) appPrefs.networkUseSocksProxy.set(cfg.useSocksProxy)
appPrefs.networkHostMode.set(cfg.hostMode.name)
appPrefs.networkRequiredHostMode.set(cfg.requiredHostMode)
appPrefs.networkTCPConnectTimeout.set(cfg.tcpConnectTimeout) appPrefs.networkTCPConnectTimeout.set(cfg.tcpConnectTimeout)
appPrefs.networkTCPTimeout.set(cfg.tcpTimeout) appPrefs.networkTCPTimeout.set(cfg.tcpTimeout)
appPrefs.networkSMPPingInterval.set(cfg.smpPingInterval) appPrefs.networkSMPPingInterval.set(cfg.smpPingInterval)
@ -1370,6 +1380,26 @@ data class NetCfg(
smpPingInterval = 600_000_000 smpPingInterval = 600_000_000
) )
} }
val onionHosts: OnionHosts get() = when {
hostMode == HostMode.Public && requiredHostMode -> OnionHosts.NEVER
hostMode == HostMode.OnionViaSocks && !requiredHostMode -> OnionHosts.PREFER
hostMode == HostMode.OnionViaSocks && requiredHostMode -> OnionHosts.REQUIRED
else -> OnionHosts.PREFER
}
fun withOnionHosts(mode: OnionHosts): NetCfg = when (mode) {
OnionHosts.NEVER ->
this.copy(hostMode = HostMode.Public, requiredHostMode = true)
OnionHosts.PREFER ->
this.copy(hostMode = HostMode.OnionViaSocks, requiredHostMode = false)
OnionHosts.REQUIRED ->
this.copy(hostMode = HostMode.OnionViaSocks, requiredHostMode = true)
}
}
enum class OnionHosts {
NEVER, PREFER, REQUIRED
} }
@Serializable @Serializable

View file

@ -1,7 +1,10 @@
package chat.simplex.app.views.call package chat.simplex.app.views.call
import android.Manifest import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.pm.ActivityInfo
import android.media.AudioManager import android.media.AudioManager
import android.util.Log import android.util.Log
import android.view.ViewGroup import android.view.ViewGroup
@ -41,6 +44,7 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
@SuppressLint("SourceLockedOrientationActivity")
@Composable @Composable
fun ActiveCallView(chatModel: ChatModel) { fun ActiveCallView(chatModel: ChatModel) {
BackHandler(onBack = { BackHandler(onBack = {
@ -122,6 +126,17 @@ fun ActiveCallView(chatModel: ChatModel) {
val call = chatModel.activeCall.value val call = chatModel.activeCall.value
if (call != null) ActiveCallOverlay(call, chatModel) if (call != null) ActiveCallOverlay(call, chatModel)
} }
val context = LocalContext.current
DisposableEffect(Unit) {
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
// Lock orientation to portrait in order to have good experience with calls
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
onDispose {
// Unlock orientation
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
}
} }
@Composable @Composable
@ -337,6 +352,8 @@ fun WebRTCView(callCommand: MutableState<WCallCommand?>, onResponse: (WVAPIMessa
val wv = webView.value val wv = webView.value
if (wv != null) processCommand(wv, WCallCommand.End) if (wv != null) processCommand(wv, WCallCommand.End)
lifecycleOwner.lifecycle.removeObserver(observer) lifecycleOwner.lifecycle.removeObserver(observer)
webView.value?.destroy()
webView.value = null
} }
} }
LaunchedEffect(callCommand.value, webView.value) { LaunchedEffect(callCommand.value, webView.value) {

View file

@ -46,15 +46,18 @@ fun IncomingCallAlertLayout(
acceptCall: () -> Unit acceptCall: () -> Unit
) { ) {
val color = if (isInDarkTheme()) IncomingCallDark else IncomingCallLight val color = if (isInDarkTheme()) IncomingCallDark else IncomingCallLight
Column(Modifier.background(color).padding(top = 16.dp, bottom = 16.dp, start = 16.dp, end = 8.dp)) { Column(Modifier.fillMaxWidth().background(color).padding(top = 16.dp, bottom = 16.dp, start = 16.dp, end = 8.dp)) {
IncomingCallInfo(invitation) IncomingCallInfo(invitation)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Row(verticalAlignment = Alignment.CenterVertically) { Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
ProfilePreview(profileOf = invitation.contact, size = 64.dp, color = Color.White) Row(Modifier.fillMaxWidth().weight(1f), verticalAlignment = Alignment.CenterVertically) {
Spacer(Modifier.fillMaxWidth().weight(1f)) ProfilePreview(profileOf = invitation.contact, size = 64.dp, color = Color.White)
CallButton(stringResource(R.string.reject), Icons.Filled.CallEnd, Color.Red, rejectCall) }
CallButton(stringResource(R.string.ignore), Icons.Filled.Close, MaterialTheme.colors.primary, ignoreCall) Row(verticalAlignment = Alignment.CenterVertically) {
CallButton(stringResource(R.string.accept), Icons.Filled.Check, SimplexGreen, acceptCall) CallButton(stringResource(R.string.reject), Icons.Filled.CallEnd, Color.Red, rejectCall)
CallButton(stringResource(R.string.ignore), Icons.Filled.Close, MaterialTheme.colors.primary, ignoreCall)
CallButton(stringResource(R.string.accept), Icons.Filled.Check, SimplexGreen, acceptCall)
}
} }
} }
} }

View file

@ -16,6 +16,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -34,6 +35,8 @@ import chat.simplex.app.SimplexApp
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
@Composable @Composable
fun ChatInfoView( fun ChatInfoView(
@ -250,10 +253,10 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
@Composable @Composable
private fun LocalAliasEditor(initialValue: String, updateValue: (String) -> Unit) { private fun LocalAliasEditor(initialValue: String, updateValue: (String) -> Unit) {
var value by remember { mutableStateOf(initialValue) } var value by rememberSaveable { mutableStateOf(initialValue) }
DefaultBasicTextField( DefaultBasicTextField(
Modifier.fillMaxWidth().padding(horizontal = 10.dp), Modifier.fillMaxWidth().padding(horizontal = 10.dp),
initialValue, value,
{ {
Text( Text(
generalGetString(R.string.text_field_set_contact_placeholder), generalGetString(R.string.text_field_set_contact_placeholder),
@ -268,8 +271,17 @@ private fun LocalAliasEditor(initialValue: String, updateValue: (String) -> Unit
) { ) {
value = it value = it
} }
LaunchedEffect(Unit) {
snapshotFlow { value }
.onEach { delay(500) } // wait a little after every new character, don't emit until user stops typing
.conflate() // get the latest value
.filter { it == value } // don't process old ones
.collect {
updateValue(value)
}
}
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { updateValue(value) } onDispose { updateValue(value) } // just in case snapshotFlow will be canceled when user presses Back too fast
} }
} }

View file

@ -32,8 +32,7 @@ import chat.simplex.app.R
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.call.* import chat.simplex.app.views.call.*
import chat.simplex.app.views.chat.group.AddGroupMembersView import chat.simplex.app.views.chat.group.*
import chat.simplex.app.views.chat.group.GroupChatInfoView
import chat.simplex.app.views.chat.item.ChatItemView import chat.simplex.app.views.chat.item.ChatItemView
import chat.simplex.app.views.chat.item.ItemAction import chat.simplex.app.views.chat.item.ItemAction
import chat.simplex.app.views.chatlist.* import chat.simplex.app.views.chatlist.*
@ -48,11 +47,13 @@ import kotlinx.datetime.Clock
@Composable @Composable
fun ChatView(chatModel: ChatModel) { fun ChatView(chatModel: ChatModel) {
var activeChat by remember { mutableStateOf(chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }) } var activeChat by remember { mutableStateOf(chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }) }
val searchText = remember { mutableStateOf("") } val searchText = rememberSaveable { mutableStateOf("") }
val user = chatModel.currentUser.value val user = chatModel.currentUser.value
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = useLinkPreviews)) } val composeState = rememberSaveable(saver = ComposeState.saver()) {
val attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) } mutableStateOf(ComposeState(useLinkPreviews = useLinkPreviews))
}
val attachmentOption = rememberSaveable { mutableStateOf<AttachmentOption?>(null) }
val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -135,12 +136,17 @@ fun ChatView(chatModel: ChatModel) {
} }
} }
}, },
openDirectChat = { contactId -> showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
val c = chatModel.chats.firstOrNull { withApi {
it.chatInfo is ChatInfo.Direct && it.chatInfo.contact.contactId == contactId val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
} ModalManager.shared.showCustomModal { close ->
if (c != null) { ModalView(
withApi { openChat(c.chatInfo, chatModel) } close = close, modifier = Modifier,
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
) {
GroupMemberInfoView(groupInfo, member, stats, chatModel, close, close)
}
}
} }
}, },
loadPrevMessages = { cInfo -> loadPrevMessages = { cInfo ->
@ -194,7 +200,7 @@ fun ChatView(chatModel: ChatModel) {
close = close, modifier = Modifier, close = close, modifier = Modifier,
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
) { ) {
AddGroupMembersView(groupInfo, chatModel, true, close) AddGroupMembersView(groupInfo, chatModel, close)
} }
} }
} }
@ -238,7 +244,7 @@ fun ChatLayout(
chatModelIncognito: Boolean, chatModelIncognito: Boolean,
back: () -> Unit, back: () -> Unit,
info: () -> Unit, info: () -> Unit,
openDirectChat: (Long) -> Unit, showMemberInfo: (GroupInfo, GroupMember) -> Unit,
loadPrevMessages: (ChatInfo) -> Unit, loadPrevMessages: (ChatInfo) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit, receiveFile: (Long) -> Unit,
@ -281,7 +287,7 @@ fun ChatLayout(
BoxWithConstraints(Modifier.fillMaxHeight().padding(contentPadding)) { BoxWithConstraints(Modifier.fillMaxHeight().padding(contentPadding)) {
ChatItemsList( ChatItemsList(
user, chat, unreadCount, composeState, chatItems, searchValue, user, chat, unreadCount, composeState, chatItems, searchValue,
useLinkPreviews, chatModelIncognito, openDirectChat, loadPrevMessages, deleteMessage, useLinkPreviews, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage,
receiveFile, joinGroup, acceptCall, markRead, setFloatingButton receiveFile, joinGroup, acceptCall, markRead, setFloatingButton
) )
} }
@ -300,8 +306,8 @@ fun ChatInfoToolbar(
addMembers: (GroupInfo) -> Unit, addMembers: (GroupInfo) -> Unit,
onSearchValueChanged: (String) -> Unit, onSearchValueChanged: (String) -> Unit,
) { ) {
var showMenu by remember { mutableStateOf(false) } var showMenu by rememberSaveable { mutableStateOf(false) }
var showSearch by remember { mutableStateOf(false) } var showSearch by rememberSaveable { mutableStateOf(false) }
val onBackClicked = { val onBackClicked = {
if (!showSearch) { if (!showSearch) {
back() back()
@ -423,7 +429,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
searchValue: State<String>, searchValue: State<String>,
useLinkPreviews: Boolean, useLinkPreviews: Boolean,
chatModelIncognito: Boolean, chatModelIncognito: Boolean,
openDirectChat: (Long) -> Unit, showMemberInfo: (GroupInfo, GroupMember) -> Unit,
loadPrevMessages: (ChatInfo) -> Unit, loadPrevMessages: (ChatInfo) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit, receiveFile: (Long) -> Unit,
@ -510,9 +516,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
Modifier Modifier
.clip(CircleShape) .clip(CircleShape)
.clickable { .clickable {
openDirectChat(contactId) showMemberInfo(chat.chatInfo.groupInfo, member)
// Scroll to first unread message when direct chat will be loaded
shouldAutoScroll = true
} }
) { ) {
MemberImage(member) MemberImage(member)
@ -826,7 +830,7 @@ fun PreviewChatLayout() {
chatModelIncognito = false, chatModelIncognito = false,
back = {}, back = {},
info = {}, info = {},
openDirectChat = {}, showMemberInfo = {_, _ -> },
loadPrevMessages = { _ -> }, loadPrevMessages = { _ -> },
deleteMessage = { _, _ -> }, deleteMessage = { _, _ -> },
receiveFile = {}, receiveFile = {},
@ -883,7 +887,7 @@ fun PreviewGroupChatLayout() {
chatModelIncognito = false, chatModelIncognito = false,
back = {}, back = {},
info = {}, info = {},
openDirectChat = {}, showMemberInfo = {_, _ -> },
loadPrevMessages = { _ -> }, loadPrevMessages = { _ -> },
deleteMessage = { _, _ -> }, deleteMessage = { _, _ -> },
receiveFile = {}, receiveFile = {},

View file

@ -19,13 +19,13 @@ import androidx.annotation.CallSuper
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon import androidx.compose.material.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AttachFile import androidx.compose.material.icons.filled.AttachFile
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.outlined.Reply import androidx.compose.material.icons.outlined.Reply
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.Saver
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -42,21 +42,26 @@ import chat.simplex.app.views.chat.item.*
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import java.io.File import java.io.File
@Serializable
sealed class ComposePreview { sealed class ComposePreview {
object NoPreview: ComposePreview() @Serializable object NoPreview: ComposePreview()
class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview() @Serializable class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview()
class ImagePreview(val image: String): ComposePreview() @Serializable class ImagePreview(val image: String): ComposePreview()
class FilePreview(val fileName: String): ComposePreview() @Serializable class FilePreview(val fileName: String): ComposePreview()
} }
@Serializable
sealed class ComposeContextItem { sealed class ComposeContextItem {
object NoContextItem: ComposeContextItem() @Serializable object NoContextItem: ComposeContextItem()
class QuotedItem(val chatItem: ChatItem): ComposeContextItem() @Serializable class QuotedItem(val chatItem: ChatItem): ComposeContextItem()
class EditingItem(val chatItem: ChatItem): ComposeContextItem() @Serializable class EditingItem(val chatItem: ChatItem): ComposeContextItem()
} }
@Serializable
data class ComposeState( data class ComposeState(
val message: String = "", val message: String = "",
val preview: ComposePreview = ComposePreview.NoPreview, val preview: ComposePreview = ComposePreview.NoPreview,
@ -99,6 +104,15 @@ data class ComposeState(
is ComposePreview.CLinkPreview -> preview.linkPreview is ComposePreview.CLinkPreview -> preview.linkPreview
else -> null else -> null
} }
companion object {
fun saver(): Saver<MutableState<ComposeState>, *> = Saver(
save = { json.encodeToString(serializer(), it.value) },
restore = {
mutableStateOf(json.decodeFromString(it))
}
)
}
} }
fun chatItemPreview(chatItem: ChatItem): ComposePreview { fun chatItemPreview(chatItem: ChatItem): ComposePreview {

View file

@ -31,7 +31,7 @@ import chat.simplex.app.views.chat.ChatInfoToolbarTitle
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
@Composable @Composable
fun AddGroupMembersView(groupInfo: GroupInfo, chatModel: ChatModel, showFooterCounter: Boolean = true, close: () -> Unit) { fun AddGroupMembersView(groupInfo: GroupInfo, chatModel: ChatModel, close: () -> Unit) {
val selectedContacts = remember { mutableStateListOf<Long>() } val selectedContacts = remember { mutableStateListOf<Long>() }
val selectedRole = remember { mutableStateOf(GroupMemberRole.Admin) } val selectedRole = remember { mutableStateOf(GroupMemberRole.Admin) }
@ -41,7 +41,6 @@ fun AddGroupMembersView(groupInfo: GroupInfo, chatModel: ChatModel, showFooterCo
contactsToAdd = getContactsToAdd(chatModel), contactsToAdd = getContactsToAdd(chatModel),
selectedContacts = selectedContacts, selectedContacts = selectedContacts,
selectedRole = selectedRole, selectedRole = selectedRole,
showFooterCounter = showFooterCounter,
inviteMembers = { inviteMembers = {
withApi { withApi {
selectedContacts.forEach { selectedContacts.forEach {
@ -79,7 +78,6 @@ fun AddGroupMembersLayout(
contactsToAdd: List<Contact>, contactsToAdd: List<Contact>,
selectedContacts: SnapshotStateList<Long>, selectedContacts: SnapshotStateList<Long>,
selectedRole: MutableState<GroupMemberRole>, selectedRole: MutableState<GroupMemberRole>,
showFooterCounter: Boolean,
inviteMembers: () -> Unit, inviteMembers: () -> Unit,
clearSelection: () -> Unit, clearSelection: () -> Unit,
addContact: (Long) -> Unit, addContact: (Long) -> Unit,
@ -124,10 +122,8 @@ fun AddGroupMembersLayout(
InviteMembersButton(inviteMembers, disabled = selectedContacts.isEmpty()) InviteMembersButton(inviteMembers, disabled = selectedContacts.isEmpty())
} }
} }
if (showFooterCounter) { SectionCustomFooter {
SectionCustomFooter { InviteSectionFooter(selectedContactsCount = selectedContacts.count(), clearSelection)
InviteSectionFooter(selectedContactsCount = selectedContacts.count(), clearSelection)
}
} }
SectionSpacer() SectionSpacer()
@ -351,7 +347,6 @@ fun PreviewAddGroupMembersLayout() {
contactsToAdd = listOf(Contact.sampleData, Contact.sampleData, Contact.sampleData), contactsToAdd = listOf(Contact.sampleData, Contact.sampleData, Contact.sampleData),
selectedContacts = remember { mutableStateListOf() }, selectedContacts = remember { mutableStateListOf() },
selectedRole = remember { mutableStateOf(GroupMemberRole.Admin) }, selectedRole = remember { mutableStateOf(GroupMemberRole.Admin) },
showFooterCounter = true,
inviteMembers = {}, inviteMembers = {},
clearSelection = {}, clearSelection = {},
addContact = {}, addContact = {},

View file

@ -51,7 +51,7 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) {
close = close, modifier = Modifier, close = close, modifier = Modifier,
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
) { ) {
AddGroupMembersView(groupInfo, chatModel, true, close) AddGroupMembersView(groupInfo, chatModel, close)
} }
} }
} }
@ -59,12 +59,12 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) {
showMemberInfo = { member -> showMemberInfo = { member ->
withApi { withApi {
val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId) val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
ModalManager.shared.showCustomModal { close -> ModalManager.shared.showCustomModal { closeCurrent ->
ModalView( ModalView(
close = close, modifier = Modifier, close = closeCurrent, modifier = Modifier,
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
) { ) {
GroupMemberInfoView(groupInfo, member, stats, chatModel, close) GroupMemberInfoView(groupInfo, member, stats, chatModel, closeCurrent) { closeCurrent(); close() }
} }
} }
} }

View file

@ -24,6 +24,7 @@ import chat.simplex.app.R
import chat.simplex.app.model.* import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.SimplexServers import chat.simplex.app.views.chat.SimplexServers
import chat.simplex.app.views.chatlist.openChat
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
@Composable @Composable
@ -32,7 +33,8 @@ fun GroupMemberInfoView(
member: GroupMember, member: GroupMember,
connStats: ConnectionStats?, connStats: ConnectionStats?,
chatModel: ChatModel, chatModel: ChatModel,
close: () -> Unit close: () -> Unit,
closeAll: () -> Unit, // Close all open windows up to ChatView
) { ) {
BackHandler(onBack = close) BackHandler(onBack = close)
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value } val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
@ -43,6 +45,22 @@ fun GroupMemberInfoView(
member, member,
connStats, connStats,
developerTools, developerTools,
openDirectChat = {
withApi {
val oldChat = chatModel.getContactChat(member.memberContactId ?: return@withApi)
if (oldChat != null) {
openChat(oldChat.chatInfo, chatModel)
} else {
var newChat = chatModel.controller.apiGetChat(ChatType.Direct, member.memberContactId) ?: return@withApi
// TODO it's not correct to blindly set network status to connected - we should manage network status in model / backend
newChat = newChat.copy(serverInfo = Chat.ServerInfo(networkStatus = Chat.NetworkStatus.Connected()))
chatModel.addChat(newChat)
chatModel.chatItems.clear()
chatModel.chatId.value = newChat.id
}
closeAll()
}
},
removeMember = { removeMemberDialog(groupInfo, member, chatModel, close) } removeMember = { removeMemberDialog(groupInfo, member, chatModel, close) }
) )
} }
@ -71,6 +89,7 @@ fun GroupMemberInfoLayout(
member: GroupMember, member: GroupMember,
connStats: ConnectionStats?, connStats: ConnectionStats?,
developerTools: Boolean, developerTools: Boolean,
openDirectChat: () -> Unit,
removeMember: () -> Unit, removeMember: () -> Unit,
) { ) {
Column( Column(
@ -87,6 +106,13 @@ fun GroupMemberInfoLayout(
} }
SectionSpacer() SectionSpacer()
SectionView {
SectionItemView {
OpenChatButton(openDirectChat)
}
}
SectionSpacer()
SectionView(title = stringResource(R.string.member_info_section_title_member)) { SectionView(title = stringResource(R.string.member_info_section_title_member)) {
InfoRow(stringResource(R.string.info_row_group), groupInfo.displayName) InfoRow(stringResource(R.string.info_row_group), groupInfo.displayName)
val conn = member.activeConn val conn = member.activeConn
@ -181,6 +207,25 @@ fun RemoveMemberButton(removeMember: () -> Unit) {
} }
} }
@Composable
fun OpenChatButton(onClick: () -> Unit) {
Row(
Modifier
.fillMaxSize()
.clickable { onClick() },
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Outlined.Message,
stringResource(R.string.button_send_direct_message),
Modifier.padding(top = 5.dp),
tint = MaterialTheme.colors.primary
)
Spacer(Modifier.size(8.dp))
Text(stringResource(R.string.button_send_direct_message), color = MaterialTheme.colors.primary)
}
}
@Preview @Preview
@Composable @Composable
fun PreviewGroupMemberInfoLayout() { fun PreviewGroupMemberInfoLayout() {
@ -190,6 +235,7 @@ fun PreviewGroupMemberInfoLayout() {
member = GroupMember.sampleData, member = GroupMember.sampleData,
connStats = null, connStats = null,
developerTools = false, developerTools = false,
openDirectChat = {},
removeMember = {} removeMember = {}
) )
} }

View file

@ -11,6 +11,7 @@ import androidx.compose.material.TextFieldDefaults.textFieldWithLabelPadding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
@ -20,8 +21,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.*
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.app.R import chat.simplex.app.R
@ -30,7 +30,7 @@ import kotlinx.coroutines.delay
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun SearchTextField(modifier: Modifier, placeholder: String, onValueChange: (String) -> Unit) { fun SearchTextField(modifier: Modifier, placeholder: String, onValueChange: (String) -> Unit) {
var searchText by remember { mutableStateOf("") } var searchText by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val keyboard = LocalSoftwareKeyboardController.current val keyboard = LocalSoftwareKeyboardController.current
@ -61,7 +61,7 @@ fun SearchTextField(modifier: Modifier, placeholder: String, onValueChange: (Str
), ),
onValueChange = { onValueChange = {
searchText = it searchText = it
onValueChange(it) onValueChange(it.text)
}, },
cursorBrush = SolidColor(colors.cursorColor(false).value), cursorBrush = SolidColor(colors.cursorColor(false).value),
visualTransformation = VisualTransformation.None, visualTransformation = VisualTransformation.None,
@ -75,13 +75,13 @@ fun SearchTextField(modifier: Modifier, placeholder: String, onValueChange: (Str
interactionSource = interactionSource, interactionSource = interactionSource,
decorationBox = @Composable { innerTextField -> decorationBox = @Composable { innerTextField ->
TextFieldDefaults.TextFieldDecorationBox( TextFieldDefaults.TextFieldDecorationBox(
value = searchText, value = searchText.text,
innerTextField = innerTextField, innerTextField = innerTextField,
placeholder = { placeholder = {
Text(placeholder) Text(placeholder)
}, },
trailingIcon = if (searchText.isNotEmpty()) {{ trailingIcon = if (searchText.text.isNotEmpty()) {{
IconButton({ searchText = ""; onValueChange("") }) { IconButton({ searchText = TextFieldValue(""); onValueChange("") }) {
Icon(Icons.Default.Close, stringResource(R.string.icon_descr_close_button), tint = MaterialTheme.colors.primary,) Icon(Icons.Default.Close, stringResource(R.string.icon_descr_close_button), tint = MaterialTheme.colors.primary,)
} }
}} else null, }} else null,

View file

@ -50,7 +50,7 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
close = close, modifier = Modifier, close = close, modifier = Modifier,
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
) { ) {
AddGroupMembersView(groupInfo, chatModel, false, close) AddGroupMembersView(groupInfo, chatModel, close)
} }
} }
} }

View file

@ -10,12 +10,14 @@ import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.model.ChatModel import chat.simplex.app.model.*
import chat.simplex.app.model.NetCfg
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.* import chat.simplex.app.views.helpers.*
@ -25,16 +27,19 @@ fun NetworkAndServersView(
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit) showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)
) { ) {
val netCfg: MutableState<NetCfg> = remember { mutableStateOf(chatModel.controller.getNetCfg()) } // It's not a state, just a one-time value. Shouldn't be used in any state-related situations
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(netCfg.value.useSocksProxy) } val netCfg = remember { chatModel.controller.getNetCfg() }
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(netCfg.useSocksProxy) }
val developerTools = chatModel.controller.appPrefs.developerTools.get() val developerTools = chatModel.controller.appPrefs.developerTools.get()
val onionHosts = remember { mutableStateOf(netCfg.onionHosts) }
NetworkAndServersLayout( NetworkAndServersLayout(
developerTools = developerTools, developerTools = developerTools,
networkUseSocksProxy = networkUseSocksProxy, networkUseSocksProxy = networkUseSocksProxy,
onionHosts = onionHosts,
showModal = showModal, showModal = showModal,
showSettingsModal = showSettingsModal, showSettingsModal = showSettingsModal,
toggleSocksProxy = { enable -> toggleSocksProxy = { enable ->
if (enable) { if (enable) {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.network_enable_socks), title = generalGetString(R.string.network_enable_socks),
@ -45,6 +50,7 @@ fun NetworkAndServersView(
chatModel.controller.apiSetNetworkConfig(NetCfg.proxyDefaults) chatModel.controller.apiSetNetworkConfig(NetCfg.proxyDefaults)
chatModel.controller.setNetCfg(NetCfg.proxyDefaults) chatModel.controller.setNetCfg(NetCfg.proxyDefaults)
networkUseSocksProxy.value = true networkUseSocksProxy.value = true
onionHosts.value = NetCfg.proxyDefaults.onionHosts
} }
} }
) )
@ -58,10 +64,29 @@ fun NetworkAndServersView(
chatModel.controller.apiSetNetworkConfig(NetCfg.defaults) chatModel.controller.apiSetNetworkConfig(NetCfg.defaults)
chatModel.controller.setNetCfg(NetCfg.defaults) chatModel.controller.setNetCfg(NetCfg.defaults)
networkUseSocksProxy.value = false networkUseSocksProxy.value = false
onionHosts.value = NetCfg.defaults.onionHosts
} }
} }
) )
} }
},
useOnion = {
val prevValue = onionHosts.value
onionHosts.value = it
updateNetworkSettingsDialog(onDismiss = {
onionHosts.value = prevValue
}) {
withApi {
val newCfg = chatModel.controller.getNetCfg().withOnionHosts(it)
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
if (res) {
chatModel.controller.setNetCfg(newCfg)
onionHosts.value = it
} else {
onionHosts.value = prevValue
}
}
}
} }
) )
} }
@ -69,9 +94,11 @@ fun NetworkAndServersView(
@Composable fun NetworkAndServersLayout( @Composable fun NetworkAndServersLayout(
developerTools: Boolean, developerTools: Boolean,
networkUseSocksProxy: MutableState<Boolean>, networkUseSocksProxy: MutableState<Boolean>,
onionHosts: MutableState<OnionHosts>,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
toggleSocksProxy: (Boolean) -> Unit toggleSocksProxy: (Boolean) -> Unit,
useOnion: (OnionHosts) -> Unit,
) { ) {
Column( Column(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
@ -89,6 +116,10 @@ fun NetworkAndServersView(
SectionItemView { SectionItemView {
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy) UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
} }
SectionDivider()
SectionItemView {
UseOnionHosts(onionHosts, networkUseSocksProxy, useOnion)
}
if (developerTools) { if (developerTools) {
SectionDivider() SectionDivider()
SettingsActionItem(Icons.Outlined.Cable, stringResource(R.string.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) }) SettingsActionItem(Icons.Outlined.Cable, stringResource(R.string.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) })
@ -129,6 +160,116 @@ fun UseSocksProxySwitch(
} }
} }
@Composable
private fun UseOnionHosts(onionHosts: MutableState<OnionHosts>, enabled: State<Boolean>, useOnion: (OnionHosts) -> Unit) {
val values = remember {
OnionHosts.values().map {
when (it) {
OnionHosts.NEVER -> OnionHosts.NEVER to generalGetString(R.string.network_use_onion_hosts_no)
OnionHosts.PREFER -> OnionHosts.PREFER to generalGetString(R.string.network_use_onion_hosts_prefer)
OnionHosts.REQUIRED -> OnionHosts.REQUIRED to generalGetString(R.string.network_use_onion_hosts_required)
}
}
}
ExposedDropDownSettingRow(
generalGetString(R.string.network_use_onion_hosts),
values,
onionHosts,
icon = Icons.Outlined.Security,
enabled = enabled,
onSelected = useOnion
)
}
@Composable
fun <T> ExposedDropDownSettingRow(
title: String,
values: List<Pair<T, String>>,
selection: State<T>,
label: String? = null,
icon: ImageVector? = null,
iconTint: Color = HighOrLowlight,
enabled: State<Boolean> = mutableStateOf(true),
onSelected: (T) -> Unit
) {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
var expanded by remember { mutableStateOf(false) }
if (icon != null) {
Icon(
icon,
"",
Modifier.padding(end = 8.dp),
tint = iconTint
)
}
Text(title, color = if (enabled.value) Color.Unspecified else HighOrLowlight)
Spacer(Modifier.fillMaxWidth().weight(1f))
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = !expanded && enabled.value
}
) {
Row(
Modifier.padding(start = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End
) {
Text(
values.first { it.first == selection.value }.second + (if (label != null) " $label" else ""),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = HighOrLowlight
)
Spacer(Modifier.size(12.dp))
Icon(
if (!expanded) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
generalGetString(R.string.icon_descr_more_button),
tint = HighOrLowlight
)
}
ExposedDropdownMenu(
modifier = Modifier.widthIn(min = 200.dp),
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
values.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
onSelected(selectionOption.first)
expanded = false
}
) {
Text(
selectionOption.second + (if (label != null) " $label" else ""),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
}
}
private fun updateNetworkSettingsDialog(onDismiss: () -> Unit, onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.update_network_settings_question),
text = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers),
confirmText = generalGetString(R.string.update_network_settings_confirmation),
onDismiss = onDismiss,
onConfirm = onConfirm,
)
}
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun PreviewNetworkAndServersLayout() { fun PreviewNetworkAndServersLayout() {
@ -138,7 +279,9 @@ fun PreviewNetworkAndServersLayout() {
networkUseSocksProxy = remember { mutableStateOf(true) }, networkUseSocksProxy = remember { mutableStateOf(true) },
showModal = { {} }, showModal = { {} },
showSettingsModal = { {} }, showSettingsModal = { {} },
toggleSocksProxy = {} toggleSocksProxy = {},
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
useOnion = {},
) )
} }
} }

View file

@ -21,6 +21,7 @@ import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import chat.simplex.app.* import chat.simplex.app.*
@ -342,11 +343,15 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
profileOf.displayName, profileOf.displayName,
style = MaterialTheme.typography.caption, style = MaterialTheme.typography.caption,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = if (stopped) HighOrLowlight else Color.Unspecified color = if (stopped) HighOrLowlight else Color.Unspecified,
maxLines = 1,
overflow = TextOverflow.Ellipsis
) )
Text( Text(
profileOf.fullName, profileOf.fullName,
color = if (stopped) HighOrLowlight else Color.Unspecified color = if (stopped) HighOrLowlight else Color.Unspecified,
maxLines = 1,
overflow = TextOverflow.Ellipsis
) )
} }
} }

View file

@ -297,6 +297,10 @@
<string name="network_enable_socks_info">Соединяться с серверами через SOCKS прокси через порт 9050? Прокси должен быть запущен до включения этой опции.</string> <string name="network_enable_socks_info">Соединяться с серверами через SOCKS прокси через порт 9050? Прокси должен быть запущен до включения этой опции.</string>
<string name="network_disable_socks">Использовать прямое соединение с Интернет?</string> <string name="network_disable_socks">Использовать прямое соединение с Интернет?</string>
<string name="network_disable_socks_info">Если вы подтвердите, серверы смогут видеть ваш IP адрес, а провайдер - с какими серверами вы соединяетесь.</string> <string name="network_disable_socks_info">Если вы подтвердите, серверы смогут видеть ваш IP адрес, а провайдер - с какими серверами вы соединяетесь.</string>
<string name="network_use_onion_hosts">Использовать .onion хосты</string>
<string name="network_use_onion_hosts_prefer">Когда возможно</string>
<string name="network_use_onion_hosts_no">Нет</string>
<string name="network_use_onion_hosts_required">Обязательно</string>
<string name="appearance_settings">Интерфейс</string> <string name="appearance_settings">Интерфейс</string>
<!-- Address Items - UserAddressView.kt --> <!-- Address Items - UserAddressView.kt -->
@ -614,6 +618,7 @@
<!-- GroupMemberInfoView.kt --> <!-- GroupMemberInfoView.kt -->
<string name="button_remove_member">Удалить члена группы</string> <string name="button_remove_member">Удалить члена группы</string>
<string name="button_send_direct_message">Отправить сообщение</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">Член группы будет удален - это действие нельзя отменить!</string> <string name="member_will_be_removed_from_group_cannot_be_undone">Член группы будет удален - это действие нельзя отменить!</string>
<string name="remove_member_confirmation">Удалить</string> <string name="remove_member_confirmation">Удалить</string>
<string name="member_info_section_title_member">ЧЛЕН ГРУППЫ</string> <string name="member_info_section_title_member">ЧЛЕН ГРУППЫ</string>

View file

@ -301,6 +301,10 @@
<string name="network_enable_socks_info">Access the servers via SOCKS proxy on port 9050? Proxy must be started before enabling this option.</string> <string name="network_enable_socks_info">Access the servers via SOCKS proxy on port 9050? Proxy must be started before enabling this option.</string>
<string name="network_disable_socks">Use direct Internet connection?</string> <string name="network_disable_socks">Use direct Internet connection?</string>
<string name="network_disable_socks_info">If you confirm, the messaging servers will be able to see your IP address, and your provider - which servers you are connecting to.</string> <string name="network_disable_socks_info">If you confirm, the messaging servers will be able to see your IP address, and your provider - which servers you are connecting to.</string>
<string name="network_use_onion_hosts">Use .onion hosts</string>
<string name="network_use_onion_hosts_prefer">When available</string>
<string name="network_use_onion_hosts_no">No</string>
<string name="network_use_onion_hosts_required">Required</string>
<string name="appearance_settings">Appearance</string> <string name="appearance_settings">Appearance</string>
<!-- Address Items - UserAddressView.kt --> <!-- Address Items - UserAddressView.kt -->
@ -615,6 +619,7 @@
<!-- GroupMemberInfoView.kt --> <!-- GroupMemberInfoView.kt -->
<string name="button_remove_member">Remove member</string> <string name="button_remove_member">Remove member</string>
<string name="button_send_direct_message">Send direct message</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">Member will be removed from group - this cannot be undone!</string> <string name="member_will_be_removed_from_group_cannot_be_undone">Member will be removed from group - this cannot be undone!</string>
<string name="remove_member_confirmation">Remove</string> <string name="remove_member_confirmation">Remove</string>
<string name="member_info_section_title_member">MEMBER</string> <string name="member_info_section_title_member">MEMBER</string>

View file

@ -25,7 +25,7 @@ struct GroupProfileView: View {
var body: some View { var body: some View {
return VStack(alignment: .leading) { return VStack(alignment: .leading) {
Text("Group profile is stored on members' devices, not on the servers.") Text("Group profile is stored on members' devices, not on the servers.")
.padding(.bottom) .padding(.vertical)
ZStack(alignment: .center) { ZStack(alignment: .center) {
ZStack(alignment: .topTrailing) { ZStack(alignment: .topTrailing) {
@ -109,7 +109,7 @@ struct GroupProfileView: View {
.onTapGesture { hideKeyboard() } .onTapGesture { hideKeyboard() }
} }
func profileNameTextEdit(_ label: String, _ name: Binding<String>) -> some View { func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding<String>) -> some View {
TextField(label, text: name) TextField(label, text: name)
.textInputAutocapitalization(.never) .textInputAutocapitalization(.never)
.disableAutocorrection(true) .disableAutocorrection(true)

View file

@ -2,7 +2,7 @@
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
<file original="en.lproj/Localizable.strings" source-language="en" target-language="en" datatype="plaintext"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="en" datatype="plaintext">
<header> <header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="13.3" build-num="13E113"/> <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="13.4.1" build-num="13F100"/>
</header> </header>
<body> <body>
<trans-unit id="&#10;" xml:space="preserve"> <trans-unit id="&#10;" xml:space="preserve">
@ -346,7 +346,7 @@
<trans-unit id="Chats" xml:space="preserve"> <trans-unit id="Chats" xml:space="preserve">
<source>Chats</source> <source>Chats</source>
<target>Chats</target> <target>Chats</target>
<note>No comment provided by engineer.</note> <note>back button to return to chats list</note>
</trans-unit> </trans-unit>
<trans-unit id="Choose file" xml:space="preserve"> <trans-unit id="Choose file" xml:space="preserve">
<source>Choose file</source> <source>Choose file</source>
@ -533,6 +533,11 @@
<target>Currently maximum supported file size is %@.</target> <target>Currently maximum supported file size is %@.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Dark" xml:space="preserve">
<source>Dark</source>
<target>Dark</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Database ID" xml:space="preserve"> <trans-unit id="Database ID" xml:space="preserve">
<source>Database ID</source> <source>Database ID</source>
<target>Database ID</target> <target>Database ID</target>
@ -1143,6 +1148,11 @@ We will be adding server redundancy to prevent lost messages.</target>
<target>Leave group?</target> <target>Leave group?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Light" xml:space="preserve">
<source>Light</source>
<target>Light</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Limitations" xml:space="preserve"> <trans-unit id="Limitations" xml:space="preserve">
<source>Limitations</source> <source>Limitations</source>
<target>Limitations</target> <target>Limitations</target>
@ -1728,6 +1738,11 @@ We will be adding server redundancy to prevent lost messages.</target>
<target>Stop chat?</target> <target>Stop chat?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="System" xml:space="preserve">
<source>System</source>
<target>System</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="TCP connection timeout" xml:space="preserve"> <trans-unit id="TCP connection timeout" xml:space="preserve">
<source>TCP connection timeout</source> <source>TCP connection timeout</source>
<target>TCP connection timeout</target> <target>TCP connection timeout</target>
@ -1828,6 +1843,11 @@ We will be adding server redundancy to prevent lost messages.</target>
<target>The sender will NOT be notified</target> <target>The sender will NOT be notified</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Theme" xml:space="preserve">
<source>Theme</source>
<target>Theme</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve">
<source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source>
<target>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</target> <target>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</target>
@ -2670,7 +2690,7 @@ SimpleX servers cannot see your profile.</target>
</file> </file>
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="en" datatype="plaintext"> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="en" datatype="plaintext">
<header> <header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="13.3" build-num="13E113"/> <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="13.4.1" build-num="13F100"/>
</header> </header>
<body> <body>
<trans-unit id="CFBundleName" xml:space="preserve"> <trans-unit id="CFBundleName" xml:space="preserve">
@ -2702,7 +2722,7 @@ SimpleX servers cannot see your profile.</target>
</file> </file>
<file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="en" datatype="plaintext"> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="en" datatype="plaintext">
<header> <header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="13.3" build-num="13E113"/> <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="13.4.1" build-num="13F100"/>
</header> </header>
<body> <body>
<trans-unit id="CFBundleDisplayName" xml:space="preserve"> <trans-unit id="CFBundleDisplayName" xml:space="preserve">

View file

@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj", "project" : "SimpleX.xcodeproj",
"targetLocale" : "en", "targetLocale" : "en",
"toolInfo" : { "toolInfo" : {
"toolBuildNumber" : "13E113", "toolBuildNumber" : "13F100",
"toolID" : "com.apple.dt.xcode", "toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode", "toolName" : "Xcode",
"toolVersion" : "13.3" "toolVersion" : "13.4.1"
}, },
"version" : "1.0" "version" : "1.0"
} }

View file

@ -2,7 +2,7 @@
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
<file original="en.lproj/Localizable.strings" source-language="en" target-language="ru" datatype="plaintext"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="ru" datatype="plaintext">
<header> <header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="13.3" build-num="13E113"/> <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="13.4.1" build-num="13F100"/>
</header> </header>
<body> <body>
<trans-unit id="&#10;" xml:space="preserve"> <trans-unit id="&#10;" xml:space="preserve">
@ -346,7 +346,7 @@
<trans-unit id="Chats" xml:space="preserve"> <trans-unit id="Chats" xml:space="preserve">
<source>Chats</source> <source>Chats</source>
<target>Чаты</target> <target>Чаты</target>
<note>No comment provided by engineer.</note> <note>back button to return to chats list</note>
</trans-unit> </trans-unit>
<trans-unit id="Choose file" xml:space="preserve"> <trans-unit id="Choose file" xml:space="preserve">
<source>Choose file</source> <source>Choose file</source>
@ -533,6 +533,11 @@
<target>Максимальный размер файла - %@.</target> <target>Максимальный размер файла - %@.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Dark" xml:space="preserve">
<source>Dark</source>
<target>Тёмная</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Database ID" xml:space="preserve"> <trans-unit id="Database ID" xml:space="preserve">
<source>Database ID</source> <source>Database ID</source>
<target>ID базы данных</target> <target>ID базы данных</target>
@ -1143,6 +1148,11 @@ We will be adding server redundancy to prevent lost messages.</source>
<target>Выйти из группы?</target> <target>Выйти из группы?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Light" xml:space="preserve">
<source>Light</source>
<target>Светлая</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Limitations" xml:space="preserve"> <trans-unit id="Limitations" xml:space="preserve">
<source>Limitations</source> <source>Limitations</source>
<target>Ограничения</target> <target>Ограничения</target>
@ -1728,6 +1738,11 @@ We will be adding server redundancy to prevent lost messages.</source>
<target>Остановить чат?</target> <target>Остановить чат?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="System" xml:space="preserve">
<source>System</source>
<target>Системная</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="TCP connection timeout" xml:space="preserve"> <trans-unit id="TCP connection timeout" xml:space="preserve">
<source>TCP connection timeout</source> <source>TCP connection timeout</source>
<target>Таймаут TCP соединения</target> <target>Таймаут TCP соединения</target>
@ -1828,6 +1843,11 @@ We will be adding server redundancy to prevent lost messages.</source>
<target>Отправитель не будет уведомлён</target> <target>Отправитель не будет уведомлён</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Theme" xml:space="preserve">
<source>Theme</source>
<target>Тема</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve">
<source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source>
<target>Это действие нельзя отменить — ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны.</target> <target>Это действие нельзя отменить — ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны.</target>
@ -2670,7 +2690,7 @@ SimpleX серверы не могут получить доступ к ваше
</file> </file>
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="ru" datatype="plaintext"> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="ru" datatype="plaintext">
<header> <header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="13.3" build-num="13E113"/> <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="13.4.1" build-num="13F100"/>
</header> </header>
<body> <body>
<trans-unit id="CFBundleName" xml:space="preserve"> <trans-unit id="CFBundleName" xml:space="preserve">
@ -2702,7 +2722,7 @@ SimpleX серверы не могут получить доступ к ваше
</file> </file>
<file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="ru" datatype="plaintext"> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="ru" datatype="plaintext">
<header> <header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="13.3" build-num="13E113"/> <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="13.4.1" build-num="13F100"/>
</header> </header>
<body> <body>
<trans-unit id="CFBundleDisplayName" xml:space="preserve"> <trans-unit id="CFBundleDisplayName" xml:space="preserve">

View file

@ -3,10 +3,10 @@
"project" : "SimpleX.xcodeproj", "project" : "SimpleX.xcodeproj",
"targetLocale" : "ru", "targetLocale" : "ru",
"toolInfo" : { "toolInfo" : {
"toolBuildNumber" : "13E113", "toolBuildNumber" : "13F100",
"toolID" : "com.apple.dt.xcode", "toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode", "toolName" : "Xcode",
"toolVersion" : "13.3" "toolVersion" : "13.4.1"
}, },
"version" : "1.0" "version" : "1.0"
} }

View file

@ -13,6 +13,11 @@
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */; }; 3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */; };
3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; }; 3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; };
5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00164328A26FBC0094D739 /* ContextMenu.swift */; }; 5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00164328A26FBC0094D739 /* ContextMenu.swift */; };
5C00166A28C119300094D739 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166528C119300094D739 /* libgmp.a */; };
5C00166B28C119300094D739 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166628C119300094D739 /* libffi.a */; };
5C00166C28C119300094D739 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166728C119300094D739 /* libgmpxx.a */; };
5C00166D28C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166828C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a */; };
5C00166E28C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166928C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a */; };
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA72837DBB3004A9677 /* CICallItemView.swift */; }; 5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA72837DBB3004A9677 /* CICallItemView.swift */; };
5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; }; 5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; };
5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.swift */; }; 5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.swift */; };
@ -127,11 +132,6 @@
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; }; 64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; }; 64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; };
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; }; 64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
64F1CC4128B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64F1CC3C28B3A99900CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5.a */; };
64F1CC4228B3A99A00CD1FB1 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64F1CC3D28B3A99900CD1FB1 /* libgmpxx.a */; };
64F1CC4328B3A99A00CD1FB1 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64F1CC3E28B3A99900CD1FB1 /* libgmp.a */; };
64F1CC4428B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64F1CC3F28B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5-ghc8.10.7.a */; };
64F1CC4528B3A99A00CD1FB1 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64F1CC4028B3A99A00CD1FB1 /* libffi.a */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -197,6 +197,11 @@
3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = "<group>"; }; 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = "<group>"; };
3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = "<group>"; }; 3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = "<group>"; };
5C00164328A26FBC0094D739 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = "<group>"; }; 5C00164328A26FBC0094D739 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = "<group>"; };
5C00166528C119300094D739 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C00166628C119300094D739 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C00166728C119300094D739 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C00166828C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a"; sourceTree = "<group>"; };
5C00166928C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a"; sourceTree = "<group>"; };
5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = "<group>"; }; 5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = "<group>"; };
5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = "<group>"; }; 5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = "<group>"; };
5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = "<group>"; }; 5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = "<group>"; };
@ -314,11 +319,6 @@
64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; }; 64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; };
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; }; 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; };
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; }; 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; };
64F1CC3C28B3A99900CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5.a"; sourceTree = "<group>"; };
64F1CC3D28B3A99900CD1FB1 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
64F1CC3E28B3A99900CD1FB1 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
64F1CC3F28B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5-ghc8.10.7.a"; sourceTree = "<group>"; };
64F1CC4028B3A99A00CD1FB1 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -351,13 +351,13 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
64F1CC4428B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5-ghc8.10.7.a in Frameworks */, 5C00166E28C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a in Frameworks */,
64F1CC4228B3A99A00CD1FB1 /* libgmpxx.a in Frameworks */,
64F1CC4328B3A99A00CD1FB1 /* libgmp.a in Frameworks */,
64F1CC4128B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5.a in Frameworks */,
64F1CC4528B3A99A00CD1FB1 /* libffi.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5C00166C28C119300094D739 /* libgmpxx.a in Frameworks */,
5C00166A28C119300094D739 /* libgmp.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5C00166B28C119300094D739 /* libffi.a in Frameworks */,
5C00166D28C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -412,11 +412,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = { 5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
64F1CC4028B3A99A00CD1FB1 /* libffi.a */, 5C00166628C119300094D739 /* libffi.a */,
64F1CC3E28B3A99900CD1FB1 /* libgmp.a */, 5C00166528C119300094D739 /* libgmp.a */,
64F1CC3D28B3A99900CD1FB1 /* libgmpxx.a */, 5C00166728C119300094D739 /* libgmpxx.a */,
64F1CC3F28B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5-ghc8.10.7.a */, 5C00166828C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a */,
64F1CC3C28B3A99900CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5.a */, 5C00166928C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a */,
); );
path = Libraries; path = Libraries;
sourceTree = "<group>"; sourceTree = "<group>";

View file

@ -404,6 +404,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Currently maximum supported file size is %@." = "Максимальный размер файла - %@."; "Currently maximum supported file size is %@." = "Максимальный размер файла - %@.";
/* No comment provided by engineer. */
"Dark" = "Тёмная";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Database export & import" = "Экспорт и импорт архива чата"; "Database export & import" = "Экспорт и импорт архива чата";
@ -824,6 +827,9 @@
/* rcv group event chat item */ /* rcv group event chat item */
"left" = "покинул(а) группу"; "left" = "покинул(а) группу";
/* No comment provided by engineer. */
"Light" = "Светлая";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Limitations" = "Ограничения"; "Limitations" = "Ограничения";
@ -1232,6 +1238,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"strike" = "зачеркнуть"; "strike" = "зачеркнуть";
/* No comment provided by engineer. */
"System" = "Системная";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Take picture" = "Сделать фото"; "Take picture" = "Сделать фото";
@ -1292,6 +1301,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"The sender will NOT be notified" = "Отправитель не будет уведомлён"; "The sender will NOT be notified" = "Отправитель не будет уведомлён";
/* No comment provided by engineer. */
"Theme" = "Тема";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Это действие нельзя отменить — ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны."; "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Это действие нельзя отменить — ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны.";

View file

@ -0,0 +1,98 @@
# SimpleX Chat v3.2 is released
**Published:** Sep 1, 2022
## What's new
- [Incognito mode](#incognito-mode)
- [assign names to your contacts](#assign-names-to-your-contacts)
- [use .onion server addresses with Tor](#using-onion-server-addresses-with-tor)
- [endless scrolling and search in chats](#endless-scrolling-and-search-in-chats)
- [choose accent color and dark mode](#choose-accent-color-and-dark-mode)
- disable notifications per contact / group
- on Android:
- swipe to reply
- reduced APK size for direct download and in F-Droid repo from 200 to 50Mb!
[Implementation audit is arranged for October](#we-ask-you-to-help-us-pay-for-3rd-party-security-audit)!
### Incognito mode
<img src="./images/20220901-incognito1.png" width="330"> <img src="./images/20220901-incognito2.png" width="330"> <img src="./images/20220901-incognito3.png" width="330">
_SimpleX is already private, so why do we need an incognito mode_, you may ask.
You indeed can choose a pseudonym as your main profile name, but there are several problems:
- many users want to have their real name as their main profile, so that their friends recognise them. SimpleX objective is to provide anonimity from the network operators, but not necessarily from your contacts.
- even if you choose a pseudonym, it would be used for all your contacts. And if two of them meet, while they cannot prove they are talking to the same person, as they use different addresses in SimpleX network to send you the messages, they could _suspect it_.
- any pseudonym you manually choose leaks some information about you, as it's not really random.
You could also use multiple chat profiles - currently you can only switch between them via export/import, we will make it easier very soon! But there are problems with multiple profiles too:
- if you make many anonymous connections, each in its own user profile, you would end up having too many profiles - it is very inconvenient to manage.
- sometimes, as your relationship with your contact evolves, you may want to share your main profile with them and have them among your friends - multiple profiles don't make it possible.
So, the new Incognito mode allows having a new random name shared with each new contact, while having them all in the same user profile, and without the hassle of managing it manually. It's like a private mode in the browsers, where you can temporarily enable it when you connect to somebody you don't trust, and then disable it when connecting to the friend who knows you. It can be turned on via the app settings - see the pictures.
I don't know any other messenger with this feature, and I always wanted to have this mode, so we are really looking forward to your feedback about it!
### Assign names to your contacts
You can now change the name under which your contacts appear in the chats. This is particularly useful when somebody connected to you using a random name you can change it to be related to the context of the connection.
### Using .onion server addresses with Tor
<img src="./images/20220901-onion1.png" width="330"> &nbsp; <img src="./images/20220901-onion2.png" width="330">
We have released support for using SOCKS proxy to access messaging servers via Tor, but previously the servers were still available via their public Internet addresses. It means that while your IP address was protected from the server, the whole Tor circuit could have been observed by some actors, and for some communication scenarios it is not desirable.
This release adds support for servers with multiple hostnames - all servers provided by SimpleX Chat now have dual addresses (one public and one .onion), and you can have your own servers available via two addresses as well - all you have to do is to install Tor client on your server and register its address with Tor. If you server has both public and .onion address, it is not really hidden, so you should enable HiddenServiceSingleHopMode to reduce the latency of connection - it protects anonymity of the people who connect to the server, but not of the server itself. The server address would include both its public and onion address, as you can see in the server addresses in the app (in the contacts pages) - you should use the same format for the addresses of your servers.
Both android and iOS app allow managing whether .onion addresses are used, and you can also enforce using .onion addresses - in this case the app will not connect to the server unless one of its hostname is .onion address. On Android, .onion addresses are used by default when SOCKS proxy is enabled.
### Endless scrolling and search in chats
Now you can access the full chat history via the app - it's embarrassing how long it took us to add it! And you can search the messages as well.
### Choose accent color and dark mode
Many of you said that blue is the worst possible color, so you can now make the app buttons and links look like you want! My favourite colours are green and orange.
And you can choose dark or light mode independently of the system settings.
## SimpleX platform
Some links to answer the most common questions:
[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers).
[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users).
[Technical details and limitations](./20220723-simplex-chat-v3.1-tor-groups-efficiency.md#privacy-technical-details-and-limitations).
[How SimpleX is different from Session, Matrix, Signal, etc.](../README.md#frequently-asked-questions).
## We ask you to help us pay for 3rd party security audit
Our great news is that we have already signed the agreement and paid for the security audit!
It is planned in October, and if there are no major issues we will publish this report straight away, otherwise - once we fix them.
This is a major expense for use - over $20,000 - I would really appreciate if you could help us cover some part of this cost with the donations.
Our promise to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We will be establishing a legal framework this year to ensure that it doesn't change if the ownership of SimpleX Chat Ltd changes at any future point.
Please consider making a donation - it will help us to raise more funds. Donating any amount, even the price of the cup of coffee, would make a huge difference for us.
It is possible to donate via:
- [GitHub](https://github.com/sponsors/simplex-chat): it is commission-free for us.
- [OpenCollective](https://opencollective.com/simplex-chat): it also accepts donations in crypto-currencies, but charges a commission.
- Monero wallet: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
Thank you,
Evgeny
SimpleX Chat founder

View file

@ -1,24 +1,32 @@
# Blog # Blog
Aug 8, 2022 [SimpleX Chat v3.1 released](./20220808-simplex-chat-v3.1-chat-groups.md) Sep 1, 2022 [v3.2: Incognito mode](./20220901-simplex-chat-v3.2-incognito-mode.md)
- finally, secret chat groups! - Incognito mode - use a new random profile name for each contact
- use .onion server addresses with Tor
- endless scrolling and search
- choose accent color and dark mode
- reduced APK size for direct download and in F-Droid repo from 200 to 46Mb!
Implementation audit is arranged for October!
Aug 8, 2022 [v3.1: chat groups](./20220808-simplex-chat-v3.1-chat-groups.md)
- finally, secret chat groups - nobody but members know they exist!
- access to messaging servers via Tor on all platforms - access to messaging servers via Tor on all platforms
- advanced network settings to optimize traffic usage - advanced network settings to optimize traffic usage
- published chat protocol - published chat protocol
- new app icons - new app icons
Jul 23, 2022 [SimpleX Chat v3.1-beta released](./20220723-simplex-chat-v3.1-tor-groups-efficiency.md) Jul 23, 2022 [v3.1-beta: access servers via Tor](./20220723-simplex-chat-v3.1-tor-groups-efficiency.md)
- terminal app: access to messaging servers via SOCKS5 proxy (e.g., Tor). - terminal app: access to messaging servers via SOCKS5 proxy (e.g., Tor).
- mobile apps: join and leave chat groups. - mobile apps: join and leave chat groups.
- optimized battery and traffic usage - up to 90x reduction! - optimized battery and traffic usage - up to 90x reduction!
- two docker configurations for self-hosted SMP servers. - two docker configurations for self-hosted SMP servers.
Jul 11, 2022 [SimpleX Chat v3 released](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md): Jul 11, 2022 [v3: instant push notifications for iOS and audio/video calls](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md):
- instant push notifications for iOS
- e2e encrypted WebRTC audio/video calls
- chat database export and import - chat database export and import
- protocol privacy and performance improvements - protocol privacy and performance improvements

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

View file

@ -1,5 +1,5 @@
name: simplex-chat name: simplex-chat
version: 3.2.0 version: 3.2.1
#synopsis: #synopsis:
#description: #description:
homepage: https://github.com/simplex-chat/simplex-chat#readme homepage: https://github.com/simplex-chat/simplex-chat#readme

View file

@ -26,6 +26,7 @@ rm $ORIG_NAME
(cd apk && zip -r -q -$level ../$ORIG_NAME .) (cd apk && zip -r -q -$level ../$ORIG_NAME .)
# Shouldn't be compressed because of Android requirement # Shouldn't be compressed because of Android requirement
(cd apk && zip -r -q -0 ../$ORIG_NAME resources.arsc) (cd apk && zip -r -q -0 ../$ORIG_NAME resources.arsc)
(cd apk && zip -r -q -0 ../$ORIG_NAME res)
#(cd apk && 7z a -r -mx=$level -tzip -x!resources.arsc ../$ORIG_NAME .) #(cd apk && 7z a -r -mx=$level -tzip -x!resources.arsc ../$ORIG_NAME .)
#(cd apk && 7z a -r -mx=0 -tzip ../$ORIG_NAME resources.arsc) #(cd apk && 7z a -r -mx=0 -tzip ../$ORIG_NAME resources.arsc)

View file

@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack -- see: https://github.com/sol/hpack
name: simplex-chat name: simplex-chat
version: 3.2.0 version: 3.2.1
category: Web, System, Services, Cryptography category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat author: simplex.chat