Merge branch 'stable' into stable-android

This commit is contained in:
Evgeny Poberezkin
2024-09-24 12:42:55 +01:00
79 changed files with 605 additions and 238 deletions
+11 -1
View File
@@ -5,12 +5,22 @@ on:
branches:
- master
- stable
- users
tags:
- "v*"
- "!*-fdroid"
- "!*-armv7a"
pull_request:
paths-ignore:
- "apps/ios"
- "apps/multiplatform"
- "blog"
- "docs"
- "fastlane"
- "images"
- "packages"
- "website"
- "README.md"
- "PRIVACY.md"
jobs:
prepare-release:
+18 -16
View File
@@ -300,25 +300,27 @@ What is already implemented:
1. Instead of user profile identifiers used by all other platforms, even the most private ones, SimpleX uses [pairwise per-queue identifiers](./docs/GLOSSARY.md#pairwise-pseudonymous-identifier) (2 addresses for each unidirectional message queue, with an optional 3rd address for push notifications on iOS, 2 queues in each connection between the users). It makes observing the network graph on the application level more difficult, as for `n` users there can be up to `n * (n-1)` message queues.
2. [End-to-end encryption](./docs/GLOSSARY.md#end-to-end-encryption) in each message queue using [NaCl cryptobox](https://nacl.cr.yp.to/box.html). This is added to allow redundancy in the future (passing each message via several servers), to avoid having the same ciphertext in different queues (that would only be visible to the attacker if TLS is compromised). The encryption keys used for this encryption are not rotated, instead we are planning to rotate the queues. Curve25519 keys are used for key negotiation.
3. [Double ratchet](./docs/GLOSSARY.md#double-ratchet-algorithm) end-to-end encryption in each conversation between two users (or group members). This is the same algorithm that is used in Signal and many other messaging apps; it provides OTR messaging with [forward secrecy](./docs/GLOSSARY.md#forward-secrecy) (each message is encrypted by its own ephemeral key) and [break-in recovery](./docs/GLOSSARY.md#post-compromise-security) (the keys are frequently re-negotiated as part of the message exchange). Two pairs of Curve448 keys are used for the initial [key agreement](./docs/GLOSSARY.md#key-agreement-protocol), initiating party passes these keys via the connection link, accepting side - in the header of the confirmation message.
4. Additional layer of encryption using NaCL cryptobox for the messages delivered from the server to the recipient. This layer avoids having any ciphertext in common between sent and received traffic of the server inside TLS (and there are no identifiers in common as well).
5. Several levels of [content padding](./docs/GLOSSARY.md#message-padding) to frustrate message size attacks.
6. All message metadata, including the time when the message was received by the server (rounded to a second) is sent to the recipients inside an encrypted envelope, so even if TLS is compromised it cannot be observed.
7. Only TLS 1.2/1.3 are allowed for client-server connections, limited to cryptographic algorithms: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448.
8. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key.
9. To protect your IP address all SimpleX Chat clients support accessing messaging servers via Tor - see [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) for more details.
10. Local database encryption with passphrase - your contacts, groups and all sent and received messages are stored encrypted. If you used SimpleX Chat before v4.0 you need to enable the encryption via the app settings.
11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections.
12. Manual messaging queue rotations to move conversation to another SMP relay.
13. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html).
14. Local files encryption.
4. [Post-quantum resistant key exchange](./docs/GLOSSARY.md#post-quantum-cryptography) in double ratchet protocol *on every ratchet step*. Read more in [this post](./blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md) and also see this [publication by Apple]( https://security.apple.com/blog/imessage-pq3/) explaining the need for post-quantum key rotation.
5. Additional layer of encryption using NaCL cryptobox for the messages delivered from the server to the recipient. This layer avoids having any ciphertext in common between sent and received traffic of the server inside TLS (and there are no identifiers in common as well).
6. Several levels of [content padding](./docs/GLOSSARY.md#message-padding) to frustrate message size attacks.
7. All message metadata, including the time when the message was received by the server (rounded to a second) is sent to the recipients inside an encrypted envelope, so even if TLS is compromised it cannot be observed.
8. Only TLS 1.2/1.3 are allowed for client-server connections, limited to cryptographic algorithms: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448.
9. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key.
10. To protect your IP address from unknown messaging relays, and for per-message transport anonymity (compared with Tor/VPN per-connection anonymity), from v6.0 all SimpleX Chat clients use private message routing by default. Read more in [this post](./blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md#private-message-routing).
11. To protect your IP address from unknown file relays, when SOCKS proxy is not enabled SimpleX Chat clients ask for a confirmation before downloading the files from unknown servers.
12. To protect your IP address from known servers all SimpleX Chat clients support accessing messaging servers via Tor - see [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) for more details.
13. Local database encryption with passphrase - your contacts, groups and all sent and received messages are stored encrypted. If you used SimpleX Chat before v4.0 you need to enable the encryption via the app settings.
14. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections.
15. Manual messaging queue rotations to move conversation to another SMP relay.
16. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html).
17. Local files encryption.
We plan to add:
1. Senders' SMP relays and recipients' XFTP relays to reduce traffic and conceal IP addresses from the relays chosen, and potentially controlled, by another party.
2. Post-quantum resistant key exchange in double ratchet protocol.
3. Automatic message queue rotation and redundancy. Currently the queues created between two users are used until the queue is manually changed by the user or contact is deleted. We are planning to add automatic queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
4. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time.
5. Reproducible builds this is the limitation of the development stack, but we will be investing into solving this problem. Users can still build all applications and services from the source code.
1. Automatic message queue rotation and redundancy. Currently the queues created between two users are used until the queue is manually changed by the user or contact is deleted. We are planning to add automatic queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
2. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time.
3. Reproducible builds this is the limitation of the development stack, but we will be investing into solving this problem. Users can still build all applications and services from the source code.
4. Recipients' XFTP relays to reduce traffic and conceal IP addresses from the relays chosen, and potentially controlled, by another party.
## For developers
-5
View File
@@ -15,16 +15,11 @@ class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
logger.debug("AppDelegate: didFinishLaunchingWithOptions")
application.registerForRemoteNotifications()
NotificationCenter.default.addObserver(self, selector: #selector(pasteboardChanged), name: UIPasteboard.changedNotification, object: nil)
removePasscodesIfReinstalled()
prepareForLaunch()
return true
}
@objc func pasteboardChanged() {
ChatModel.shared.pasteboardHasStrings = UIPasteboard.general.hasStrings
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02hhx", $0) }.joined()
logger.debug("AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken \(token)")
-1
View File
@@ -183,7 +183,6 @@ final class ChatModel: ObservableObject {
@Published var stopPreviousRecPlay: URL? = nil // coordinates currently playing source
@Published var draft: ComposeState?
@Published var draftChatId: String?
@Published var pasteboardHasStrings: Bool = UIPasteboard.general.hasStrings
@Published var networkInfo = UserNetworkInfo(networkType: .other, online: true)
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
@@ -24,6 +24,7 @@ private enum MigrationFromState: Equatable {
}
private enum MigrateFromDeviceViewAlert: Identifiable {
case finishMigration(_ fileId: Int64, _ ctrl: chat_ctrl)
case deleteChat(_ title: LocalizedStringKey = "Delete chat profile?", _ text: LocalizedStringKey = "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.")
case startChat(_ title: LocalizedStringKey = "Start chat?", _ text: LocalizedStringKey = "Warning: starting chat on multiple devices is not supported and will cause message delivery failures")
@@ -38,6 +39,7 @@ private enum MigrateFromDeviceViewAlert: Identifiable {
var id: String {
switch self {
case .finishMigration: return "finishMigration"
case let .deleteChat(title, text): return "\(title) \(text)"
case let .startChat(title, text): return "\(title) \(text)"
@@ -138,6 +140,15 @@ struct MigrateFromDevice: View {
}
.alert(item: $alert) { alert in
switch alert {
case let .finishMigration(fileId, ctrl):
return Alert(
title: Text("Remove archive?"),
message: Text("The uploaded database archive will be permanently removed from the servers."),
primaryButton: .destructive(Text("Continue")) {
finishMigration(fileId, ctrl)
},
secondaryButton: .cancel()
)
case let .startChat(title, text):
return Alert(
title: Text(title),
@@ -318,7 +329,7 @@ struct MigrateFromDevice: View {
Text("Cancel migration").foregroundColor(.red)
}
}
Button(action: { finishMigration(fileId, ctrl) }) {
Button(action: { alert = .finishMigration(fileId, ctrl) }) {
settingsRow("checkmark", color: theme.colors.secondary) {
Text("Finalize migration").foregroundColor(theme.colors.primary)
}
@@ -93,7 +93,6 @@ struct MigrateToDevice: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@Binding var migrationState: MigrationToState?
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
@State private var alert: MigrateToDeviceViewAlert?
@@ -102,6 +101,7 @@ struct MigrateToDevice: View {
// Prevent from hiding the view until migration is finished or app deleted
@State private var backDisabled: Bool = false
@State private var showQRCodeScanner: Bool = true
@State private var pasteboardHasStrings = UIPasteboard.general.hasStrings
var body: some View {
VStack {
@@ -197,10 +197,8 @@ struct MigrateToDevice: View {
}
}
}
if developerTools {
Section(header: Text("Or paste archive link").foregroundColor(theme.colors.secondary)) {
pasteLinkView()
}
Section(header: Text("Or paste archive link").foregroundColor(theme.colors.secondary)) {
pasteLinkView()
}
}
}
@@ -218,7 +216,7 @@ struct MigrateToDevice: View {
} label: {
Text("Tap to paste link")
}
.disabled(!ChatModel.shared.pasteboardHasStrings)
.disabled(!pasteboardHasStrings)
.frame(maxWidth: .infinity, alignment: .center)
}
@@ -487,6 +485,9 @@ struct MigrateToDevice: View {
do {
if !hasChatCtrl() {
chatInitControllerRemovingDatabases()
} else if ChatModel.shared.chatRunning == true {
// cannot delete storage if chat is running
try await apiStopChat()
}
try await apiDeleteStorage()
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
@@ -556,11 +557,22 @@ struct MigrateToDevice: View {
do {
try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory())
MigrationToDeviceState.save(nil)
appSettings.importIntoApp()
try SimpleX.startChat(refreshInvitations: true)
AlertManager.shared.showAlertMsg(title: "Chat migrated!", message: "Finalize migration on another device.")
try ObjC.catchException {
appSettings.importIntoApp()
}
do {
try SimpleX.startChat(refreshInvitations: true)
AlertManager.shared.showAlertMsg(title: "Chat migrated!", message: "Finalize migration on another device.")
} catch let error {
AlertManager.shared.showAlert(Alert(title: Text("Error starting chat"), message: Text(responseError(error))))
}
} catch let error {
AlertManager.shared.showAlert(Alert(title: Text("Error starting chat"), message: Text(responseError(error))))
logger.error("Error importing settings: \(error.localizedDescription)")
AlertManager.shared.showAlert(
Alert(
title: Text("Error migrating settings"),
message: Text ("Not all settings were migrated. Repeat migration if you need them.") + Text("\n\n") + Text(responseError(error)))
)
}
hideView()
}
@@ -199,7 +199,7 @@ func chatContactType(chat: Chat) -> ContactType {
case .contactRequest:
return .request
case let .direct(contact):
if contact.activeConn == nil && contact.profile.contactLink != nil {
if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active {
return .card
} else if contact.chatDeleted {
return .chatDeleted
@@ -296,6 +296,7 @@ private struct ConnectView: View {
@Binding var pastedLink: String
@Binding var alert: NewChatViewAlert?
@State private var sheet: PlanAndConnectActionSheet?
@State private var pasteboardHasStrings = UIPasteboard.general.hasStrings
var body: some View {
List {
@@ -332,7 +333,7 @@ private struct ConnectView: View {
} label: {
Text("Tap to paste link")
}
.disabled(!ChatModel.shared.pasteboardHasStrings)
.disabled(!pasteboardHasStrings)
.frame(maxWidth: .infinity, alignment: .center)
} else {
linkTextView(pastedLink)
@@ -52,10 +52,10 @@ extension AppSettings {
profileImageCornerRadiusGroupDefault.set(val)
def.setValue(val, forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS)
}
if let val = uiColorScheme { def.setValue(val, forKey: DEFAULT_CURRENT_THEME) }
if let val = uiDarkColorScheme { def.setValue(val, forKey: DEFAULT_SYSTEM_DARK_THEME) }
if let val = uiCurrentThemeIds { def.setValue(val, forKey: DEFAULT_CURRENT_THEME_IDS) }
if let val = uiThemes { def.setValue(val.skipDuplicates(), forKey: DEFAULT_THEME_OVERRIDES) }
if let val = uiColorScheme { currentThemeDefault.set(val) }
if let val = uiDarkColorScheme { systemDarkThemeDefault.set(val) }
if let val = uiCurrentThemeIds { currentThemeIdsDefault.set(val) }
if let val = uiThemes { themeOverridesDefault.set(val.skipDuplicates()) }
if let val = oneHandUI { groupDefaults.setValue(val, forKey: GROUP_DEFAULT_ONE_HAND_UI) }
}
@@ -177,6 +177,8 @@
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; };
64EEB0F72C353F1C00972D62 /* ServersSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */; };
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
8C01E9C12C8EFC33008A4B0A /* objc.m in Sources */ = {isa = PBXBuildFile; fileRef = 8C01E9C02C8EFC33008A4B0A /* objc.m */; };
8C01E9C22C8EFF8F008A4B0A /* objc.h in Headers */ = {isa = PBXBuildFile; fileRef = 8C01E9BF2C8EFBB6008A4B0A /* objc.h */; };
8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */; };
8C74C3E52C1B900600039E77 /* ThemeTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7E3CE32C0DEAC400BFF63A /* ThemeTypes.swift */; };
8C74C3E72C1B901900039E77 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C852B072C1086D100BA61E8 /* Color.swift */; };
@@ -516,6 +518,8 @@
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; };
64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersSummaryView.swift; sourceTree = "<group>"; };
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; };
8C01E9BF2C8EFBB6008A4B0A /* objc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = objc.h; sourceTree = "<group>"; };
8C01E9C02C8EFC33008A4B0A /* objc.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = objc.m; sourceTree = "<group>"; };
8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
8C74C3EB2C1B92A900039E77 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
8C74C3ED2C1B942300039E77 /* ChatWallpaper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWallpaper.swift; sourceTree = "<group>"; };
@@ -976,6 +980,8 @@
5CE2BA96284537A800EC33A6 /* dummy.m */,
5CD67B8D2B0E858A00C510B1 /* hs_init.h */,
5CD67B8E2B0E858A00C510B1 /* hs_init.c */,
8C01E9BF2C8EFBB6008A4B0A /* objc.h */,
8C01E9C02C8EFC33008A4B0A /* objc.m */,
);
path = SimpleXChat;
sourceTree = "<group>";
@@ -1113,6 +1119,7 @@
files = (
5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */,
5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */,
8C01E9C22C8EFF8F008A4B0A /* objc.h in Headers */,
5CE2BA952845354B00EC33A6 /* SimpleX.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1539,6 +1546,7 @@
8C74C3E72C1B901900039E77 /* Color.swift in Sources */,
5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */,
5CE2BA91284533A300EC33A6 /* Notifications.swift in Sources */,
8C01E9C12C8EFC33008A4B0A /* objc.m in Sources */,
5CE2BA79284530CC00EC33A6 /* SimpleXChat.docc in Sources */,
5CE2BA90284533A300EC33A6 /* JSON.swift in Sources */,
5CE2BA8B284533A300EC33A6 /* ChatTypes.swift in Sources */,
+1
View File
@@ -10,6 +10,7 @@
#define SimpleX_h
#include "hs_init.h"
#include "objc.h"
extern void hs_init(int argc, char **argv[]);
+20
View File
@@ -0,0 +1,20 @@
//
// objc.h
// SimpleX (iOS)
//
// Created by Stanislav Dmitrenko on 09.09.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
#ifndef objc_h
#define objc_h
#import <Foundation/Foundation.h>
@interface ObjC : NSObject
+ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error;
@end
#endif /* objc_h */
+25
View File
@@ -0,0 +1,25 @@
//
// objc.m
// SimpleXChat
//
// Created by Stanislav Dmitrenko on 09.09.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
#import "objc.h"
@implementation ObjC
// https://stackoverflow.com/a/36454808
+ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error {
@try {
tryBlock();
return YES;
}
@catch (NSException *exception) {
*error = [[NSError alloc] initWithDomain: exception.name code: 0 userInfo: exception.userInfo];
return NO;
}
}
@end
+11 -11
View File
@@ -15,7 +15,7 @@ android {
namespace = "chat.simplex.app"
minSdk = 26
//noinspection OldTargetApi
targetSdk = 33
targetSdk = 34
// !!!
// skip version code after release to F-Droid, as it uses two version codes
versionCode = (extra["android.version_code"] as String).toInt()
@@ -126,29 +126,29 @@ android {
dependencies {
implementation(project(":common"))
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.core:core-ktx:1.13.1")
//implementation("androidx.compose.ui:ui:${rootProject.extra["compose.version"] as String}")
//implementation("androidx.compose.material:material:$compose_version")
//implementation("androidx.compose.ui:ui-tooling-preview:$compose_version")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-process:2.7.0")
implementation("androidx.activity:activity-compose:1.8.2")
val workVersion = "2.9.0"
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4")
implementation("androidx.lifecycle:lifecycle-process:2.8.4")
implementation("androidx.activity:activity-compose:1.9.1")
val workVersion = "2.9.1"
implementation("androidx.work:work-runtime-ktx:$workVersion")
implementation("androidx.work:work-multiprocess:$workVersion")
implementation("com.jakewharton:process-phoenix:2.2.0")
implementation("com.jakewharton:process-phoenix:3.0.0")
//Camera Permission
implementation("com.google.accompanist:accompanist-permissions:0.23.0")
implementation("com.google.accompanist:accompanist-permissions:0.34.0")
//implementation("androidx.compose.material:material-icons-extended:$compose_version")
//implementation("androidx.compose.ui:ui-util:$compose_version")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
//androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
debugImplementation("androidx.compose.ui:ui-tooling:1.6.4")
}
@@ -21,6 +21,12 @@
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<!-- Requirements that allows to specify foreground service types -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<application
android:name="SimplexApp"
android:allowBackup="false"
@@ -133,7 +139,9 @@
android:name=".SimplexService"
android:enabled="true"
android:exported="false"
android:stopWithTask="false"></service>
android:stopWithTask="false"
android:foregroundServiceType="remoteMessaging"
/>
<!-- SimplexService restart on reboot -->
@@ -141,7 +149,9 @@
android:name=".CallService"
android:enabled="true"
android:exported="false"
android:stopWithTask="false"/>
android:stopWithTask="false"
android:foregroundServiceType="mediaPlayback|microphone|camera|remoteMessaging"
/>
<receiver
android:name=".CallService$CallActionReceiver"
@@ -2,17 +2,18 @@ package chat.simplex.app
import android.app.*
import android.content.*
import android.content.pm.ServiceInfo
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.*
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import chat.simplex.app.model.NtfManager.EndCallAction
import chat.simplex.app.views.call.CallActivity
import chat.simplex.common.model.NotificationPreviewMode
import chat.simplex.common.platform.*
import chat.simplex.common.views.call.CallState
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.datetime.Instant
@@ -34,7 +35,7 @@ class CallService: Service() {
} else {
Log.d(TAG, "null intent. Probably restarted by the system.")
}
startForeground(CALL_SERVICE_ID, serviceNotification)
ServiceCompat.startForeground(this, CALL_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType())
return START_STICKY
}
@@ -42,8 +43,7 @@ class CallService: Service() {
super.onCreate()
Log.d(TAG, "Call service created")
notificationManager = createNotificationChannel()
updateNotification()
startForeground(CALL_SERVICE_ID, serviceNotification)
ServiceCompat.startForeground(this, CALL_SERVICE_ID, updateNotification(), foregroundServiceType())
}
override fun onDestroy() {
@@ -69,7 +69,14 @@ class CallService: Service() {
}
}
fun updateNotification() {
private fun createNotificationIfNeeded(): Notification {
val ntf = serviceNotification
if (ntf != null) return ntf
return updateNotification()
}
fun updateNotification(): Notification {
val call = chatModel.activeCall.value
val previewMode = appPreferences.notificationPreviewMode.get()
val title = if (previewMode == NotificationPreviewMode.HIDDEN.name)
@@ -83,8 +90,31 @@ class CallService: Service() {
else
base64ToBitmap(image).asAndroidBitmap()
serviceNotification = createNotification(title, text, largeIcon, call?.connectedAt)
startForeground(CALL_SERVICE_ID, serviceNotification)
val ntf = createNotification(title, text, largeIcon, call?.connectedAt)
serviceNotification = ntf
ServiceCompat.startForeground(this, CALL_SERVICE_ID, ntf, foregroundServiceType())
return ntf
}
private fun foregroundServiceType(): Int {
val call = chatModel.activeCall.value
return if (call == null) {
if (Build.VERSION.SDK_INT >= 34) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
} else {
0
}
} else if (Build.VERSION.SDK_INT >= 30) {
if (call.supportsVideo()) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
} else {
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
}
} else if (Build.VERSION.SDK_INT >= 29) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
} else {
0
}
}
private fun createNotificationChannel(): NotificationManager? {
@@ -54,7 +54,7 @@ class MainActivity: FragmentActivity() {
SimplexApp.context.schedulePeriodicWakeUp()
}
override fun onNewIntent(intent: Intent?) {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
processIntent(intent)
processExternalIntent(intent)
@@ -5,9 +5,8 @@ import android.util.Log
import androidx.work.*
import chat.simplex.app.SimplexService.Companion.showPassphraseNotification
import chat.simplex.common.model.ChatController
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.DBMigrationResult
import chat.simplex.common.platform.chatModel
import chat.simplex.common.platform.initChatControllerOnStart
import chat.simplex.common.views.helpers.DatabaseUtils
import kotlinx.coroutines.*
import java.util.Date
@@ -30,12 +29,12 @@ object MessagesFetcherWorker {
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
.build()
WorkManager.getInstance(SimplexApp.context).enqueueUniqueWork(UNIQUE_WORK_TAG, ExistingWorkPolicy.REPLACE, periodicWorkRequest)
SimplexApp.context.getWorkManagerInstance().enqueueUniqueWork(UNIQUE_WORK_TAG, ExistingWorkPolicy.REPLACE, periodicWorkRequest)
}
fun cancelAll() {
Log.d(TAG, "Worker: canceled all tasks")
WorkManager.getInstance(SimplexApp.context).cancelUniqueWork(UNIQUE_WORK_TAG)
SimplexApp.context.getWorkManagerInstance().cancelUniqueWork(UNIQUE_WORK_TAG)
}
}
@@ -7,7 +7,6 @@ import chat.simplex.common.platform.Log
import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.*
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.graphics.Color
@@ -120,7 +119,10 @@ class SimplexApp: Application(), LifecycleEventObserver {
* */
if (chatModel.chatRunning.value != false &&
chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete &&
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE &&
// New installation passes all checks above and tries to start the service which is not needed at all
// because preferred notification type is not yet chosen. So, check that the user has initialized db already
appPrefs.newDatabaseInitialized.get()
) {
SimplexService.start()
}
@@ -161,7 +163,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
.addTag(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC)
.build()
Log.d(TAG, "ServiceStartWorker: Scheduling period work every ${SimplexService.SERVICE_START_WORKER_INTERVAL_MINUTES} minutes")
WorkManager.getInstance(context)?.enqueueUniquePeriodicWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC, workPolicy, work)
getWorkManagerInstance().enqueueUniquePeriodicWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC, workPolicy, work)
}
fun schedulePeriodicWakeUp() = CoroutineScope(Dispatchers.Default).launch {
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.*
import android.content.*
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.net.Uri
import android.os.*
import android.os.SystemClock
@@ -15,8 +16,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import androidx.work.*
import chat.simplex.app.model.NtfManager
import chat.simplex.common.AppLock
import chat.simplex.common.helpers.requiresIgnoringBattery
import chat.simplex.common.model.ChatController
@@ -52,18 +55,15 @@ class SimplexService: Service() {
} else {
Log.d(TAG, "null intent. Probably restarted by the system.")
}
startForeground(SIMPLEX_SERVICE_ID, serviceNotification)
ServiceCompat.startForeground(this, SIMPLEX_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType())
return START_STICKY // to restart if killed
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "Simplex service created")
val title = generalGetString(MR.strings.simplex_service_notification_title)
val text = generalGetString(MR.strings.simplex_service_notification_text)
notificationManager = createNotificationChannel()
serviceNotification = createNotification(title, text)
startForeground(SIMPLEX_SERVICE_ID, serviceNotification)
createNotificationIfNeeded()
ServiceCompat.startForeground(this, SIMPLEX_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType())
/**
* The reason [stopAfterStart] exists is because when the service is not called [startForeground] yet, and
* we call [stopSelf] on the same service, [ForegroundServiceDidNotStartInTimeException] will be thrown.
@@ -103,6 +103,26 @@ class SimplexService: Service() {
super.onDestroy()
}
private fun createNotificationIfNeeded(): Notification {
val ntf = serviceNotification
if (ntf != null) return ntf
val title = generalGetString(MR.strings.simplex_service_notification_title)
val text = generalGetString(MR.strings.simplex_service_notification_text)
notificationManager = createNotificationChannel()
val newNtf = createNotification(title, text)
serviceNotification = newNtf
return newNtf
}
private fun foregroundServiceType(): Int {
return if (Build.VERSION.SDK_INT >= 34) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
} else {
0
}
}
private fun startService() {
Log.d(TAG, "SimplexService startService")
if (wakeLock != null || isCheckingNewMessages) return
@@ -272,7 +292,7 @@ class SimplexService: Service() {
fun scheduleStart(context: Context) {
Log.d(TAG, "Enqueuing work to start subscriber service")
val workManager = WorkManager.getInstance(context)
val workManager = context.getWorkManagerInstance()
val startServiceRequest = OneTimeWorkRequest.Builder(ServiceStartWorker::class.java).build()
workManager.enqueueUniqueWork(WORK_NAME_ONCE, ExistingWorkPolicy.KEEP, startServiceRequest) // Unique avoids races!
}
@@ -292,6 +312,10 @@ class SimplexService: Service() {
}
private suspend fun serviceAction(action: Action) {
if (!NtfManager.areNotificationsEnabledInSystem()) {
Log.d(TAG, "SimplexService serviceAction: ${action.name}. Notifications are not enabled in OS yet, not starting service")
return
}
Log.d(TAG, "SimplexService serviceAction: ${action.name}")
withContext(Dispatchers.IO) {
Intent(androidAppContext, SimplexService::class.java).also {
@@ -53,7 +53,7 @@ object NtfManager {
private val msgNtfTimeoutMs = 30000L
init {
if (manager.areNotificationsEnabled()) createNtfChannelsMaybeShowAlert()
if (areNotificationsEnabledInSystem()) createNtfChannelsMaybeShowAlert()
}
private fun callNotificationChannel(channelId: String, channelName: String): NotificationChannel {
@@ -287,6 +287,8 @@ object NtfManager {
}
}
fun areNotificationsEnabledInSystem() = manager.areNotificationsEnabled()
/**
* This function creates notifications channels. On Android 13+ calling it for the first time will trigger system alert,
* The alert asks a user to allow or disallow to show notifications for the app. That's why it should be called only when the user
@@ -120,6 +120,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
return grantedAudio && grantedCamera
}
@Deprecated("Was deprecated in OS")
override fun onBackPressed() {
if (isOnLockScreenNow()) {
super.onBackPressed()
@@ -139,6 +140,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
}
override fun onUserLeaveHint() {
super.onUserLeaveHint()
// On Android 12+ PiP is enabled automatically when a user hides the app
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R && callSupportsVideo() && platform.androidPictureInPictureAllowed()) {
enterPictureInPictureMode()
@@ -248,6 +250,9 @@ fun CallActivityView() {
)
if (permissionsState.allPermissionsGranted) {
ActiveCallView()
LaunchedEffect(Unit) {
activity.startServiceAndBind()
}
} else {
CallPermissionsView(remember { m.activeCallViewIsCollapsed }.value, callSupportsVideo()) {
withBGApi { chatModel.callManager.endCall(call) }
@@ -285,11 +290,6 @@ fun CallActivityView() {
AlertManager.shared.showInView()
}
}
LaunchedEffect(call == null) {
if (call != null) {
activity.startServiceAndBind()
}
}
LaunchedEffect(invitation, call, switchingCall, showCallView) {
if (!switchingCall && invitation == null && (!showCallView || call == null)) {
Log.d(TAG, "CallActivityView: finishing activity")
@@ -2,6 +2,5 @@
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="highOrLowLight">#8b8786</color>
<color name="window_background_dark">#121212</color>
</resources>
+9 -9
View File
@@ -61,8 +61,8 @@ kotlin {
val androidMain by getting {
kotlin.srcDir("build/generated/moko/androidMain/src")
dependencies {
implementation("androidx.activity:activity-compose:1.8.2")
val workVersion = "2.9.0"
implementation("androidx.activity:activity-compose:1.9.1")
val workVersion = "2.9.1"
implementation("androidx.work:work-runtime-ktx:$workVersion")
implementation("com.google.accompanist:accompanist-insets:0.30.1")
@@ -78,22 +78,22 @@ kotlin {
//Camera Permission
implementation("com.google.accompanist:accompanist-permissions:0.34.0")
implementation("androidx.webkit:webkit:1.10.0")
implementation("androidx.webkit:webkit:1.11.0")
// GIFs support
implementation("io.coil-kt:coil-compose:2.6.0")
implementation("io.coil-kt:coil-gif:2.6.0")
implementation("com.jakewharton:process-phoenix:2.2.0")
implementation("com.jakewharton:process-phoenix:3.0.0")
val cameraXVersion = "1.3.2"
val cameraXVersion = "1.3.4"
implementation("androidx.camera:camera-core:${cameraXVersion}")
implementation("androidx.camera:camera-camera2:${cameraXVersion}")
implementation("androidx.camera:camera-lifecycle:${cameraXVersion}")
implementation("androidx.camera:camera-view:${cameraXVersion}")
// Calls lifecycle listener
implementation("androidx.lifecycle:lifecycle-process:2.4.1")
implementation("androidx.lifecycle:lifecycle-process:2.8.4")
}
}
val desktopMain by getting {
@@ -102,7 +102,7 @@ kotlin {
implementation("com.github.Dansoftowner:jSystemThemeDetector:3.8")
implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT")
implementation("org.slf4j:slf4j-simple:2.0.12")
implementation("uk.co.caprica:vlcj:4.8.2")
implementation("uk.co.caprica:vlcj:4.8.3")
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd:efb2ebf85a")
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd-websocket:efb2ebf85a")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
@@ -119,8 +119,8 @@ android {
defaultConfig {
minSdk = 26
}
testOptions.targetSdk = 33
lint.targetSdk = 33
testOptions.targetSdk = 34
lint.targetSdk = 34
val isAndroid = gradle.startParameter.taskNames.find {
val lower = it.lowercase()
lower.contains("release") || lower.startsWith("assemble") || lower.startsWith("install")
@@ -6,6 +6,8 @@ import android.net.LocalServerSocket
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.fragment.app.FragmentActivity
import androidx.work.Configuration
import androidx.work.WorkManager
import java.io.*
import java.lang.ref.WeakReference
import java.util.*
@@ -72,3 +74,16 @@ fun initHaskell() {
initHS()
}
fun Context.getWorkManagerInstance(): WorkManager {
// https://github.com/OneSignal/OneSignal-Android-SDK/pull/2052/files
// https://github.com/OneSignal/OneSignal-Android-SDK/issues/1672
if (!WorkManager.isInitialized()) {
try {
WorkManager.initialize(this, Configuration.Builder().build())
} catch (e: IllegalStateException) {
Log.e(TAG, "Error initializing WorkManager: ${e.stackTraceToString()}")
}
}
return WorkManager.getInstance(this)
}
@@ -2,7 +2,6 @@ package chat.simplex.common.views.usersettings
import SectionView
import androidx.compose.runtime.Composable
import androidx.work.WorkManager
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.*
@@ -33,7 +32,7 @@ fun restartApp() {
}
private fun shutdownApp() {
WorkManager.getInstance(androidAppContext).cancelAllWork()
androidAppContext.getWorkManagerInstance().cancelAllWork()
platform.androidServiceSafeStop()
Runtime.getRuntime().exit(0)
}
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<solid android:color="@color/highOrLowLight" />
<solid android:color="#8b8786" />
<size android:width="1dp" />
</shape>
@@ -411,7 +411,9 @@ object ChatModel {
// remove from current chat
if (chatId.value == cInfo.id) {
chatItems.removeAll {
val remove = it.id == cItem.id
// We delete taking into account meta.createdAt to make sure we will not be in situation when two items with the same id will be deleted
// (it can happen if already deleted chat item in backend still in the list and new one came with the same (re-used) chat item id)
val remove = it.id == cItem.id && it.meta.createdAt == cItem.meta.createdAt
if (remove) { AudioPlayer.stop(it) }
remove
}
@@ -5316,6 +5316,7 @@ abstract class TerminalItem {
val date: Instant = Clock.System.now()
abstract val label: String
abstract val details: String
val createdAtNanos: Long = System.nanoTime()
class Cmd(override val id: Long, override val remoteHostId: Long?, val cmd: CC): TerminalItem() {
override val label get() = "> ${cmd.cmdString}"
@@ -132,7 +132,7 @@ fun TerminalLog() {
}
val clipboard = LocalClipboardManager.current
LazyColumnWithScrollBar(reverseLayout = true) {
items(reversedTerminalItems) { item ->
items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item ->
val rhId = item.remoteHostId
val rhIdStr = if (rhId == null) "" else "$rhId "
Text(
@@ -105,6 +105,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> {
val perChatTheme = remember(chatInfo, CurrentColors.value.base) { if (chatInfo is ChatInfo.Direct) chatInfo.contact.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else null }
val overrides = if (perChatTheme != null) ThemeManager.currentColors(null, perChatTheme, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) else null
val fullDeleteAllowed = remember(chatInfo) { chatInfo.featureEnabled(ChatFeature.FullDelete) }
SimpleXThemeOverride(overrides ?: CurrentColors.collectAsState().value) {
ChatLayout(
remoteHostId = remoteHostId,
@@ -142,10 +143,15 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
chatInfo = chatInfo,
deleteItems = { canDeleteForAll ->
val itemIds = selectedChatItems.value
val questionText =
if (!canDeleteForAll || fullDeleteAllowed || chatInfo is ChatInfo.Local)
generalGetString(MR.strings.delete_messages_cannot_be_undone_warning)
else
generalGetString(MR.strings.delete_messages_mark_deleted_warning)
if (itemIds != null) {
deleteMessagesAlertDialog(
itemIds.sorted(),
generalGetString(if (itemIds.size == 1) MR.strings.delete_message_mark_deleted_warning else MR.strings.delete_messages_mark_deleted_warning),
questionText = questionText,
forAll = canDeleteForAll,
deleteMessages = { ids, forAll ->
deleteMessages(chatRh, chatInfo, ids, forAll, moderate = false) {
@@ -998,7 +1004,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
}
)
LazyColumnWithScrollBar(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) {
itemsIndexed(reversedChatItems, key = { _, item -> item.id }) { i, cItem ->
itemsIndexed(reversedChatItems, key = { _, item -> item.id to item.meta.createdAt.toEpochMilliseconds() }) { i, cItem ->
CompositionLocalProvider(
// Makes horizontal and vertical scrolling to coexist nicely.
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
@@ -40,7 +40,7 @@ import kotlinx.coroutines.launch
const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
@Composable
fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit, onSearchClicked: () -> Unit) {
fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit, onSearchClicked: () -> Unit) {
BackHandler(onBack = close)
// TODO derivedStateOf?
val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId }
@@ -249,9 +249,8 @@ fun AddGroupMembersButton(
)
}
@Composable
fun GroupChatInfoLayout(
fun ModalData.GroupChatInfoLayout(
chat: Chat,
groupInfo: GroupInfo,
currentUser: User,
@@ -272,12 +271,12 @@ fun GroupChatInfoLayout(
close: () -> Unit = { ModalManager.closeAllModalsEverywhere()},
onSearchClicked: () -> Unit
) {
val listState = rememberLazyListState()
val listState = remember { appBarHandler.listState }
val scope = rememberCoroutineScope()
KeyChangeEffect(chat.id) {
scope.launch { listState.scrollToItem(0) }
}
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
val searchText = remember { stateGetOrPut("searchText") { TextFieldValue() } }
val filteredMembers = remember(members) {
derivedStateOf {
val s = searchText.value.text.trim().lowercase()
@@ -674,7 +673,7 @@ private fun SearchRowView(
@Composable
fun PreviewGroupChatInfoLayout() {
SimpleXTheme {
GroupChatInfoLayout(
ModalData().GroupChatInfoLayout(
chat = Chat(
remoteHostId = null,
chatInfo = ChatInfo.Direct.sampleData,
@@ -135,7 +135,7 @@ fun ChatItemView(
}
fun deleteMessageQuestionText(): String {
return if (!sent || fullDeleteAllowed) {
return if (!sent || fullDeleteAllowed || cInfo is ChatInfo.Local) {
generalGetString(MR.strings.delete_message_cannot_be_undone_warning)
} else {
generalGetString(MR.strings.delete_message_mark_deleted_warning)
@@ -637,7 +637,7 @@ fun DeleteItemAction(
}
deleteMessagesAlertDialog(
itemIds,
generalGetString(if (itemIds.size == 1) MR.strings.delete_message_mark_deleted_warning else MR.strings.delete_messages_mark_deleted_warning),
generalGetString(MR.strings.delete_messages_cannot_be_undone_warning),
forAll = false,
deleteMessages = { ids, _ -> deleteMessages(ids) }
)
@@ -186,7 +186,7 @@ fun ErrorChatListItem() {
suspend fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) {
when {
contact.activeConn == null && contact.profile.contactLink != null -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true)
contact.activeConn == null && contact.profile.contactLink != null && contact.active -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true)
else -> openChat(rhId, ChatInfo.Direct(contact), chatModel)
}
}
@@ -356,7 +356,8 @@ sealed class WallpaperType {
private fun drawToBitmap(image: ImageBitmap, imageScale: Float, tint: Color, size: Size, density: Float, layoutDirection: LayoutDirection): ImageBitmap {
val quality = if (appPlatform.isAndroid) FilterQuality.High else FilterQuality.Low
val drawScope = CanvasDrawScope()
val bitmap = ImageBitmap(size.width.toInt(), size.height.toInt())
// Don't allow to make zero size because it crashes the app when reducing a size of a window on desktop
val bitmap = ImageBitmap(size.width.toInt().coerceAtLeast(1), size.height.toInt().coerceAtLeast(1))
val canvas = Canvas(bitmap)
drawScope.draw(
density = Density(density),
@@ -71,12 +71,6 @@ fun SearchTextField(
}
}
DisposableEffect(Unit) {
onDispose {
if (searchText.value.text.isNotEmpty()) onValueChange("")
}
}
val colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Unspecified,
textColor = MaterialTheme.colors.onBackground,
@@ -349,7 +349,15 @@ private fun MutableState<MigrationFromState>.LinkShownView(fileId: Long, link: S
text = stringResource(MR.strings.migrate_from_device_finalize_migration),
textColor = MaterialTheme.colors.primary,
click = {
finishMigration(fileId, ctrl)
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.migrate_from_device_remove_archive_question),
text = generalGetString(MR.strings.migrate_from_device_uploaded_archive_will_be_removed),
confirmText = generalGetString(MR.strings.continue_to_next_step),
destructive = true,
onConfirm = {
finishMigration(fileId, ctrl)
}
)
}
) {}
SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_archive_will_be_deleted))
@@ -199,10 +199,8 @@ private fun MutableState<MigrationToState?>.PasteOrScanLinkView() {
SectionSpacer()
}
if (appPlatform.isDesktop || appPreferences.developerTools.get()) {
SectionView(stringResource(if (appPlatform.isAndroid) MR.strings.or_paste_archive_link else MR.strings.paste_archive_link).uppercase()) {
PasteLinkView()
}
SectionView(stringResource(if (appPlatform.isAndroid) MR.strings.or_paste_archive_link else MR.strings.paste_archive_link).uppercase()) {
PasteLinkView()
}
}
@@ -561,7 +559,7 @@ private fun MutableState<MigrationToState?>.startDownloading(
)
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
} else {
Log.d(TAG, "unsupported error: ${msg.responseType}")
Log.d(TAG, "unsupported error: ${msg.responseType}, ${json.encodeToString(msg.chatError)}")
}
}
else -> Log.d(TAG, "unsupported event: ${msg.responseType}")
@@ -36,11 +36,13 @@ import chat.simplex.common.views.chatlist.ScrollDirection
import chat.simplex.common.views.contacts.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import java.net.URI
@Composable
fun NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) {
fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) {
val oneHandUI = remember { appPrefs.oneHandUI.state }
val keyboardState by getKeyboardState()
val showToolbarInOneHandUI = remember { derivedStateOf { keyboardState == KeyboardState.Closed && oneHandUI.value } }
@@ -97,7 +99,7 @@ fun chatContactType(chat: Chat): ContactType {
val contact = cInfo.contact
when {
contact.activeConn == null && contact.profile.contactLink != null -> ContactType.CARD
contact.activeConn == null && contact.profile.contactLink != null && contact.active -> ContactType.CARD
contact.chatDeleted -> ContactType.CHAT_DELETED
contact.contactStatus == ContactStatus.Active -> ContactType.RECENT
else -> ContactType.UNLISTED
@@ -111,10 +113,8 @@ private fun filterContactTypes(c: List<Chat>, contactTypes: List<ContactType>):
return c.filter { chat -> contactTypes.contains(chatContactType(chat)) }
}
private var lazyListState = 0 to 0
@Composable
private fun NewChatSheetLayout(
private fun ModalData.NewChatSheetLayout(
rh: RemoteHostInfo?,
addContact: () -> Unit,
scanPaste: () -> Unit,
@@ -122,7 +122,23 @@ private fun NewChatSheetLayout(
close: () -> Unit,
) {
val oneHandUI = remember { appPrefs.oneHandUI.state }
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
val listState = remember { appBarHandler.listState }
// This is workaround of an issue when position of a list is not restored (when going back to that screen) when a header exists.
// Upon returning back, this code returns correct index and position if number of items is the same
LaunchedEffect(Unit) {
val prevIndex = listState.firstVisibleItemIndex
val prevOffset = listState.firstVisibleItemScrollOffset
val total = listState.layoutInfo.totalItemsCount
if (prevIndex == 0 && prevOffset == 0) return@LaunchedEffect
snapshotFlow { listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
.filter { it == 0 to 0 }
.collect {
if (total <= listState.layoutInfo.totalItemsCount) {
listState.scrollToItem(prevIndex, prevOffset)
}
cancel()
}
}
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
val searchShowingSimplexLink = remember { mutableStateOf(false) }
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
@@ -536,7 +552,7 @@ private fun contactTypesSearchTargets(baseContactTypes: List<ContactType>, searc
}
@Composable
private fun DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats: () -> Unit, close: () -> Unit) {
private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats: () -> Unit, close: () -> Unit) {
val oneHandUI = remember { appPrefs.oneHandUI.state }
val keyboardState by getKeyboardState()
val showToolbarInOneHandUI = remember { derivedStateOf { keyboardState == KeyboardState.Closed && oneHandUI.value } }
@@ -558,7 +574,7 @@ private fun DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats: () -> Un
}
}
) { contentPadding ->
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
val listState = remember { appBarHandler.listState }
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
val searchShowingSimplexLink = remember { mutableStateOf(false) }
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
@@ -723,6 +739,6 @@ fun ActionButton(
@Composable
private fun PreviewNewChatSheet() {
SimpleXTheme {
NewChatSheet(rh = null, close = {})
ModalData().NewChatSheet(rh = null, close = {})
}
}
@@ -22,8 +22,8 @@
<string name="messages_section_description">ينطبق هذا الإعداد على الرسائل الموجودة في ملف تعريف الدردشة الحالي الخاص بك</string>
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">منصة الرسائل والتطبيقات تحمي خصوصيتك وأمنك.</string>
<string name="profile_is_only_shared_with_your_contacts">يتم مشاركة ملف التعريف مع جهات اتصالك فقط.</string>
<string name="member_role_will_be_changed_with_notification">سيتم تغيير الدور إلى \"%s\". سيتم إبلاغ كل فرد في المجموعة.</string>
<string name="member_role_will_be_changed_with_invitation">سيتم تغيير الدور إلى \"%s\". سيستلم العضو دعوة جديدة.</string>
<string name="member_role_will_be_changed_with_notification">سيتم تغيير الدور إلى "%s". سيتم إبلاغ كل فرد في المجموعة.</string>
<string name="member_role_will_be_changed_with_invitation">سيتم تغيير الدور إلى "%s". سيستلم العضو دعوة جديدة.</string>
<string name="smp_servers_per_user">خوادم الاتصالات الجديدة لملف تعريف الدردشة الحالي الخاص بك</string>
<string name="switch_receiving_address_desc">سيتم تغيير عنوان الاستلام إلى خادم مختلف. سيتم إكمال تغيير العنوان بعد اتصال المرسل بالإنترنت.</string>
<string name="this_link_is_not_a_valid_connection_link">هذا الرابط ليس رابط اتصال صالح!</string>
@@ -767,7 +767,7 @@
<string name="no_contacts_selected">لم تٌحدد جهات اتصال</string>
<string name="v4_6_group_moderation_descr">يمكّن للمشرف الآن:
\n- حذف رسائل الأعضاء.
\n- تعطيل الأعضاء (دور \"المراقب\")</string>
\n- تعطيل الأعضاء (دور "المراقب")</string>
<string name="settings_notifications_mode_title">خدمة الإشعار</string>
<string name="chat_preferences_off">غير مفعّل</string>`
<string name="chat_preferences_on">مفعل</string>
@@ -1076,7 +1076,7 @@
<string name="stop_file__action">إيقاف الملف</string>
<string name="stop_snd_file__title">التوقف عن إرسال الملف؟</string>
<string name="icon_descr_address">عنوان SimpleX</string>
<string name="disable_onion_hosts_when_not_supported"><![CDATA[اضبط <i>استخدم مضيفي .onion</i> إلى \"لا\" إذا كان وكيل SOCKS لا يدعمها.]]></string>
<string name="disable_onion_hosts_when_not_supported"><![CDATA[اضبط <i>استخدم مضيفي .onion</i> إلى "لا" إذا كان وكيل SOCKS لا يدعمها.]]></string>
<string name="share_with_contacts">مشاركة مع جهات الاتصال</string>
<string name="shutdown_alert_question">إيقاف التشغيل؟</string>
<string name="network_socks_proxy_settings">إعدادات وكيل SOCKS</string>
@@ -1406,7 +1406,7 @@
<string name="connected_desktop">سطح المكتب متصل</string>
<string name="multicast_connect_automatically">اتصل تلقائيًا</string>
<string name="desktop_address">عنوان سطح المكتب</string>
<string name="marked_deleted_items_description">وضّع علامة \"محذوفة\" على %d من الرسائل</string>
<string name="marked_deleted_items_description">وضّع علامة "محذوفة" على %d من الرسائل</string>
<string name="discover_on_network">اكتشف عبر الشبكة المحلية</string>
<string name="connect_plan_connect_via_link">اتصل عبر الرابط؟</string>
<string name="connect_plan_connect_to_yourself">اتصل بنفسك؟</string>
@@ -318,6 +318,7 @@
<string name="delete_message__question">Delete message?</string>
<string name="delete_messages__question">Delete %d messages?</string>
<string name="delete_message_cannot_be_undone_warning">Message will be deleted - this cannot be undone!</string>
<string name="delete_messages_cannot_be_undone_warning">Messages will be deleted - this cannot be undone!</string>
<string name="delete_message_mark_deleted_warning">Message will be marked for deletion. The recipient(s) will be able to reveal this message.</string>
<string name="delete_messages_mark_deleted_warning">Messages will be marked for deletion. The recipient(s) will be able to reveal these messages.</string>
<string name="delete_member_message__question">Delete member message?</string>
@@ -1556,8 +1557,8 @@
<string name="change_verb">Change</string>
<string name="switch_verb">Switch</string>
<string name="change_member_role_question">Change group role?</string>
<string name="member_role_will_be_changed_with_notification">The role will be changed to \"%s\". Everyone in the group will be notified.</string>
<string name="member_role_will_be_changed_with_invitation">The role will be changed to \"%s\". The member will receive a new invitation.</string>
<string name="member_role_will_be_changed_with_notification">The role will be changed to "%s". Everyone in the group will be notified.</string>
<string name="member_role_will_be_changed_with_invitation">The role will be changed to "%s". The member will receive a new invitation.</string>
<string name="connect_via_member_address_alert_title">Connect directly?</string>
<string name="connect_via_member_address_alert_desc">Сonnection request will be sent to this group member.</string>
<string name="error_removing_member">Error removing member</string>
@@ -1893,7 +1894,7 @@
<string name="v4_6_audio_video_calls">Audio and video calls</string>
<string name="v4_6_audio_video_calls_descr">Support bluetooth and other improvements.</string>
<string name="v4_6_group_moderation">Group moderation</string>
<string name="v4_6_group_moderation_descr">Now admins can:\n- delete members\' messages.\n- disable members (\"observer\" role)</string>
<string name="v4_6_group_moderation_descr">Now admins can:\n- delete members\' messages.\n- disable members ("observer" role)</string>
<string name="v4_6_group_welcome_message">Group welcome message</string>
<string name="v4_6_group_welcome_message_descr">Set the message shown to new members!</string>
<string name="v4_6_reduced_battery_usage">Further reduced battery usage</string>
@@ -2174,6 +2175,8 @@
<string name="migrate_from_device_creating_archive_link">Creating archive link</string>
<string name="migrate_from_device_cancel_migration">Cancel migration</string>
<string name="migrate_from_device_finalize_migration">Finalize migration</string>
<string name="migrate_from_device_remove_archive_question">Remove archive?</string>
<string name="migrate_from_device_uploaded_archive_will_be_removed">The uploaded database archive will be permanently removed from the servers.</string>
<string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Choose <i>Migrate from another device</i> on the new device and scan QR code.]]></string>
<string name="migrate_from_device_or_share_this_file_link">Or securely share this file link</string>
<string name="migrate_from_device_delete_database_from_device">Delete database from this device</string>
@@ -482,7 +482,7 @@
<string name="sending_delivery_receipts_will_be_enabled_all_profiles">Изпращането на потвърждениe за доставка ще бъде активирано за всички контакти във всички видими чат профили.</string>
<string name="send_receipts">Изпращане на потвърждениe за доставка</string>
<string name="you_can_enable_delivery_receipts_later">Можете да активирате по-късно през Настройки</string>
<string name="you_can_enable_delivery_receipts_later_alert">Можете да ги активирате по-късно през настройките за \"Поверителност и сигурност\" на приложението.</string>
<string name="you_can_enable_delivery_receipts_later_alert">Можете да ги активирате по-късно през настройките за "Поверителност и сигурност" на приложението.</string>
<string name="database_downgrade_warning">Предупреждение: Може да загубите някои данни!</string>
<string name="enter_correct_passphrase">Въведи правилна парола.</string>
<string name="feature_enabled_for_you">активирано за вас</string>
@@ -517,7 +517,7 @@
<string name="snd_conn_event_ratchet_sync_agreed">криптирането е съгласувано за %s</string>
<string name="button_edit_group_profile">Редактирай групов профил</string>
<string name="share_text_disappears_at">Изчезва в: %s</string>
<string name="member_role_will_be_changed_with_invitation">Ролята ще бъде променена на \"%s\". Членът ще получи нова покана.</string>
<string name="member_role_will_be_changed_with_invitation">Ролята ще бъде променена на "%s". Членът ще получи нова покана.</string>
<string name="conn_level_desc_direct">директна</string>
<string name="renegotiate_encryption">Предоговори криптирането</string>
<string name="group_display_name_field">Въведи име на групата:</string>
@@ -918,7 +918,7 @@
<string name="member_will_be_removed_from_group_cannot_be_undone">Членът ще бъде премахнат от групата - това не може да бъде отменено!</string>
<string name="item_info_no_text">няма текст</string>
<string name="button_remove_member">Острани член</string>
<string name="member_role_will_be_changed_with_notification">Ролята ще бъде променена на \"%s\". Всички в групата ще бъдат уведомени.</string>
<string name="member_role_will_be_changed_with_notification">Ролята ще бъде променена на "%s". Всички в групата ще бъдат уведомени.</string>
<string name="users_delete_data_only">Само данни за локален профил</string>
<string name="users_delete_with_connections">Профилни и сървърни връзки</string>
<string name="user_mute">Без звук</string>
@@ -936,7 +936,7 @@
<string name="v4_6_hidden_chat_profiles_descr">Защитете чат профилите с парола!</string>
<string name="v4_6_group_moderation_descr">Сега администраторите могат:
\n- да изтриват съобщения на членове.
\n- да деактивират членове (роля \"наблюдател\")</string>
\n- да деактивират членове (роля "наблюдател")</string>
<string name="v4_6_reduced_battery_usage_descr">Очаквайте скоро още подобрения!</string>
<string name="v5_0_polish_interface">Полски интерфейс</string>
<string name="v5_1_message_reactions">Реакции на съобщения</string>
@@ -1155,7 +1155,7 @@
<string name="this_link_is_not_a_valid_connection_link">Този линк не е валиден линк за връзка!</string>
<string name="your_chat_profiles">Вашите чат профили</string>
<string name="smp_servers_per_user">Сървърите за нови връзки на текущия ви чат профил</string>
<string name="to_reveal_profile_enter_password">За да покажете скрития профил, въведете пълната парола в полето за търсене на страницата \"Вашите чат профили\".</string>
<string name="to_reveal_profile_enter_password">За да покажете скрития профил, въведете пълната парола в полето за търсене на страницата "Вашите чат профили".</string>
<string name="profile_is_only_shared_with_your_contacts">Профилът се споделя само с вашите контакти.</string>
<string name="delete_files_and_media_desc">Това действие не може да бъде отменено - всички получени и изпратени файлове и медия ще бъдат изтрити. Снимките с ниска разделителна способност ще бъдат запазени.</string>
<string name="tap_to_activate_profile">Докосни за активиране на профил.</string>
@@ -1646,7 +1646,7 @@
<string name="v5_6_quantum_resistant_encryption">Квантово устойчиво криптиране</string>
<string name="v5_6_app_data_migration">Миграция на данните от приложението</string>
<string name="v5_6_app_data_migration_descr">Мигрирайте към друго устройство чрез QR код.</string>
<string name="v5_6_picture_in_picture_calls">Обаждания \"картина в картина\"</string>
<string name="v5_6_picture_in_picture_calls">Обаждания "картина в картина"</string>
<string name="v5_6_safer_groups">По-безопасни групи</string>
<string name="v5_6_safer_groups_descr">Администраторите могат да блокират член за всички.</string>
<string name="migrate_to_device_downloading_details">Подробности за линка се изтеглят</string>
@@ -462,7 +462,7 @@
<string name="rcv_group_event_member_left">odešel</string>
<string name="clear_contacts_selection_button">Vyčistit</string>
<string name="switch_verb">Přepnout</string>
<string name="member_role_will_be_changed_with_notification">Role bude změněna na \"%s\". Všichni ve skupině budou informováni.</string>
<string name="member_role_will_be_changed_with_notification">Role bude změněna na "%s". Všichni ve skupině budou informováni.</string>
<string name="error_removing_member">Chyba při odebrání člena</string>
<string name="error_saving_group_profile">Chyba při ukládání profilu skupiny</string>
<string name="network_option_seconds_label">vteřiny</string>
@@ -857,7 +857,7 @@
<string name="role_in_group">Role</string>
<string name="change_role">Změnit roli</string>
<string name="change_verb">Změnit</string>
<string name="member_role_will_be_changed_with_invitation">Role bude změněna na \"%s\". Člen obdrží novou pozvánku.</string>
<string name="member_role_will_be_changed_with_invitation">Role bude změněna na "%s". Člen obdrží novou pozvánku.</string>
<string name="error_changing_role">Chyba při změně role</string>
<string name="conn_level_desc_direct">přímo</string>
<string name="sending_via">Odesíláno přes</string>
@@ -972,7 +972,7 @@
<string name="v4_6_reduced_battery_usage_descr">Další vylepšení již brzy!</string>
<string name="v4_6_group_moderation_descr">Nyní mohou správci:
\n- mazat zprávy členů.
\n- zakázat členy (role \"pozorovatel\")</string>
\n- zakázat členy (role "pozorovatel")</string>
<string name="save_profile_password">Uložit heslo profilu</string>
<string name="user_mute">Ztlumit</string>
<string name="v4_6_hidden_chat_profiles_descr">Chraňte své chat profily heslem!</string>
@@ -792,8 +792,8 @@
<string name="change_verb">Ändern</string>
<string name="switch_verb">Wechseln</string>
<string name="change_member_role_question">Die Mitgliederrolle ändern?</string>
<string name="member_role_will_be_changed_with_notification">Die Mitgliederrolle wird auf \"%s\" geändert. Alle Mitglieder der Gruppe werden benachrichtigt.</string>
<string name="member_role_will_be_changed_with_invitation">Die Mitgliederrolle wird auf \"%s\" geändert. Das Mitglied wird eine neue Einladung erhalten.</string>
<string name="member_role_will_be_changed_with_notification">Die Mitgliederrolle wird auf "%s" geändert. Alle Mitglieder der Gruppe werden benachrichtigt.</string>
<string name="member_role_will_be_changed_with_invitation">Die Mitgliederrolle wird auf "%s" geändert. Das Mitglied wird eine neue Einladung erhalten.</string>
<string name="error_removing_member">Fehler beim Entfernen des Mitglieds</string>
<string name="error_changing_role">Fehler beim Ändern der Rolle</string>
<string name="info_row_group">Gruppe</string>
@@ -1074,7 +1074,7 @@
<string name="you_will_still_receive_calls_and_ntfs">Sie können Anrufe und Benachrichtigungen auch von stummgeschalteten Profilen empfangen, solange diese aktiv sind.</string>
<string name="group_welcome_title">Begrüßungsmeldung</string>
<string name="you_can_hide_or_mute_user_profile">Sie können ein Benutzerprofil verbergen oder stummschalten für das Menü gedrückt halten.</string>
<string name="to_reveal_profile_enter_password">Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite \"Ihre Chat-Profile\" ein, um Ihr verborgenes Profil zu sehen.</string>
<string name="to_reveal_profile_enter_password">Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite "Ihre Chat-Profile" ein, um Ihr verborgenes Profil zu sehen.</string>
<string name="invalid_migration_confirmation">Migrations-Bestätigung ungültig</string>
<string name="upgrade_and_open_chat">Aktualisieren und den Chat öffnen</string>
<string name="confirm_database_upgrades">Datenbank-Aktualisierungen bestätigen</string>
@@ -1151,7 +1151,7 @@
<string name="network_socks_toggle_use_socks_proxy">SOCKS-Proxy nutzen</string>
<string name="la_lock_mode">SimpleX-Sperrmodus</string>
<string name="lock_not_enabled">SimpleX-Sperre ist nicht aktiviert!</string>
<string name="disable_onion_hosts_when_not_supported"><![CDATA[Setzen Sie <i>Verwende .onion-Hosts</i> auf \"Nein\", wenn der SOCKS-Proxy sie nicht unterstützt.]]></string>
<string name="disable_onion_hosts_when_not_supported"><![CDATA[Setzen Sie <i>Verwende .onion-Hosts</i> auf "Nein", wenn der SOCKS-Proxy sie nicht unterstützt.]]></string>
<string name="submit_passcode">Bestätigen</string>
<string name="la_mode_system">System</string>
<string name="la_could_not_be_verified">Sie können nicht überprüft werden bitte versuchen Sie es nochmal.</string>
@@ -726,7 +726,7 @@
<string name="icon_descr_sent_msg_status_unauthorized_send">envío no autorizado</string>
<string name="set_contact_name">Escribe un nombre para el contacto</string>
<string name="unknown_error">Error desconocido</string>
<string name="member_role_will_be_changed_with_notification">El rol del miembro cambiará a \"%s\" y se notificará al grupo.</string>
<string name="member_role_will_be_changed_with_notification">El rol del miembro cambiará a "%s" y se notificará al grupo.</string>
<string name="v4_2_security_assessment_desc">La seguridad de SimpleX Chat ha sido auditada por Trail of Bits.</string>
<string name="v4_4_disappearing_messages_desc">Los mensajes enviados se eliminarán una vez transcurrido el tiempo establecido.</string>
<string name="ntf_channel_messages">Mensajes de chat SimpleX</string>
@@ -814,7 +814,7 @@
<string name="update_database_passphrase">Actualizar contraseña base de datos</string>
<string name="group_invitation_tap_to_join_incognito">Pulsa para unirte en modo incógnito</string>
<string name="switch_verb">Cambiar</string>
<string name="member_role_will_be_changed_with_invitation">El rol del miembro cambiará a \"%s\" y recibirá una invitación nueva.</string>
<string name="member_role_will_be_changed_with_invitation">El rol del miembro cambiará a "%s" y recibirá una invitación nueva.</string>
<string name="update_network_settings_confirmation">Actualizar</string>
<string name="update_network_settings_question">¿Actualizar la configuración de red\?</string>
<string name="trying_to_connect_to_server_to_receive_messages">Intentando conectar con el servidor para recibir mensajes de este contacto.</string>
@@ -992,7 +992,7 @@
<string name="you_will_still_receive_calls_and_ntfs">Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos.</string>
<string name="v4_6_group_moderation_descr">Ahora los administradores pueden:
\n- eliminar mensajes de los miembros.
\n- desactivar el rol miembro (a rol \"observador\")</string>
\n- desactivar el rol miembro (a rol "observador")</string>
<string name="to_reveal_profile_enter_password">Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda del menú Mis perfiles.</string>
<string name="database_upgrade">Actualización de la base de datos</string>
<string name="database_downgrade">Volviendo a versión anterior de la base de datos</string>
@@ -757,7 +757,7 @@
<string name="v4_6_reduced_battery_usage_descr">Lisää parannuksia on tulossa pian!</string>
<string name="v4_6_group_moderation_descr">Nyt järjestelmänvalvojat voivat:
\n- poistaa jäsenten viestit.
\n- poista jäsenet käytöstä (\"tarkkailija\" rooli)</string>
\n- poista jäsenet käytöstä ("tarkkailija" rooli)</string>
<string name="save_and_notify_group_members">Tallenna ja ilmoita ryhmän jäsenille</string>
<string name="stop_chat_confirmation">Lopeta</string>
<string name="stop_chat_to_export_import_or_delete_chat_database">Pysäytä keskustelut viedäksesi, tuodaksesi tai poistaaksesi keskustelujen tietokannan. Et voi vastaanottaa ja lähettää viestejä, kun keskustelut on pysäytetty.</string>
@@ -1108,8 +1108,8 @@
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Viestintä- ja sovellusalusta, joka suojaa yksityisyyttäsi ja tietoturvaasi.</string>
<string name="icon_descr_video_call">videopuhelu</string>
<string name="group_info_section_title_num_members"> %1$s JÄSENET</string>
<string name="member_role_will_be_changed_with_notification">Rooli muuttuu muotoon \"%s\". Kaikille ryhmän jäsenille ilmoitetaan asiasta.</string>
<string name="member_role_will_be_changed_with_invitation">Rooli muuttuu muotoon \"%s\". Jäsen saa uuden kutsun.</string>
<string name="member_role_will_be_changed_with_notification">Rooli muuttuu muotoon "%s". Kaikille ryhmän jäsenille ilmoitetaan asiasta.</string>
<string name="member_role_will_be_changed_with_invitation">Rooli muuttuu muotoon "%s". Jäsen saa uuden kutsun.</string>
<string name="voice_prohibited_in_this_chat">Ääniviestit ovat kiellettyjä tässä keskustelussa.</string>
<string name="v4_3_voice_messages">Ääniviestit</string>
<string name="trying_to_connect_to_server_to_receive_messages">Yritetään muodostaa yhteys palvelimeen, jota käytetään viestien vastaanottamiseen tältä kontaktilta.</string>
@@ -972,7 +972,7 @@
<string name="v4_6_hidden_chat_profiles">Profils de chat cachés</string>
<string name="v4_6_group_moderation_descr">Désormais, les administrateurs peuvent :
\n- supprimer les messages des membres.
\n- désactiver des membres (rôle \"observateur\")</string>
\n- désactiver des membres (rôle "observateur")</string>
<string name="save_welcome_message_question">Enregistrer le message d\'accueil?</string>
<string name="v4_6_group_welcome_message_descr">Choisissez un message à l\'attention des nouveaux membres !</string>
<string name="hide_profile">Masquer le profil</string>
@@ -822,8 +822,8 @@
<string name="theme_system">Sistema</string>
<string name="network_option_tcp_connection_timeout">Scadenza connessione TCP</string>
<string name="group_is_decentralized">Completamente decentralizzato: visibile solo ai membri.</string>
<string name="member_role_will_be_changed_with_notification">Il ruolo verrà cambiato in \"%s\". Tutti i membri del gruppo riceveranno una notifica.</string>
<string name="member_role_will_be_changed_with_invitation">Il ruolo verrà cambiato in \"%s\". Il membro riceverà un nuovo invito.</string>
<string name="member_role_will_be_changed_with_notification">Il ruolo verrà cambiato in "%s". Tutti i membri del gruppo riceveranno una notifica.</string>
<string name="member_role_will_be_changed_with_invitation">Il ruolo verrà cambiato in "%s". Il membro riceverà un nuovo invito.</string>
<string name="update_network_settings_confirmation">Aggiorna</string>
<string name="update_network_settings_question">Aggiornare le impostazioni di rete\?</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">L\'aggiornamento delle impostazioni riconnetterà il client a tutti i server.</string>
@@ -978,11 +978,11 @@
<string name="v4_6_chinese_spanish_interface_descr">Grazie agli utenti contribuite via Weblate!</string>
<string name="hidden_profile_password">Password del profilo nascosta</string>
<string name="save_profile_password">Salva la password del profilo</string>
<string name="to_reveal_profile_enter_password">Per rivelare il tuo profilo nascosto, inserisci una password completa in un campo di ricerca nella pagina \"I tuoi profili di chat\".</string>
<string name="to_reveal_profile_enter_password">Per rivelare il tuo profilo nascosto, inserisci una password completa in un campo di ricerca nella pagina "I tuoi profili di chat".</string>
<string name="password_to_show">Password per mostrare</string>
<string name="v4_6_group_moderation_descr">Ora gli amministratori possono:
\n- eliminare i messaggi dei membri.
\n- disattivare i membri (ruolo \"osservatore\")</string>
\n- disattivare i membri (ruolo "osservatore")</string>
<string name="hide_profile">Nascondi il profilo</string>
<string name="confirm_password">Conferma password</string>
<string name="error_updating_user_privacy">Errore nell\'aggiornamento della privacy dell\'utente</string>
@@ -1015,7 +1015,7 @@
<string name="group_invitation_tap_to_join">הקישו כדי להצטרף</string>
<string name="network_option_tcp_connection_timeout">תום זמן חיבור TCP</string>
<string name="periodic_notifications_desc">האפליקציה בודקת הודעות חדשות מעת לעת - היא משתמשת בכמה אחוזים מהסוללה ביום. האפליקציה לא משתמשת בהתראות דחיפה - נתונים מהמכשיר שלך לא נשלחים לשרתים.</string>
<string name="member_role_will_be_changed_with_notification">התפקיד ישתנה ל־\"%s\". כל חברי הקבוצה יקבלו הודעה על כך.</string>
<string name="member_role_will_be_changed_with_notification">התפקיד ישתנה ל־"%s". כל חברי הקבוצה יקבלו הודעה על כך.</string>
<string name="to_connect_via_link_title">כדי להתחבר באמצעות קישור</string>
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">פלטפורמת ההודעות והיישומים המגנה על הפרטיות והאבטחה שלך.</string>
<string name="alert_text_msg_bad_id">המזהה של ההודעה הבאה שגוי (קטן או שווה להודעה הקודמת).
@@ -1027,7 +1027,7 @@
<string name="thank_you_for_installing_simplex">תודה שהתקנתם את SimpleX Chat!</string>
<string name="this_link_is_not_a_valid_connection_link">קישור זה אינו קישור חיבור תקין!</string>
<string name="theme_colors_section_title">צבעי ערכת נושא</string>
<string name="member_role_will_be_changed_with_invitation">התפקיד ישתנה ל־\"%s\". החבר יקבל הזמנה חדשה.</string>
<string name="member_role_will_be_changed_with_invitation">התפקיד ישתנה ל־"%s". החבר יקבל הזמנה חדשה.</string>
<string name="smp_servers_per_user">השרתים לחיבורים חדשים של פרופיל הצ׳אט הנוכחי שלך</string>
<string name="first_platform_without_user_ids">הפלטפורמה הראשונה ללא כל מזהי משתמש - פרטית בעיצובה.</string>
<string name="next_generation_of_private_messaging">הדור הבא של תקשורת פרטית</string>
@@ -1836,7 +1836,7 @@
<string name="color_mode_system">מערכת</string>
<string name="error_showing_desktop_notification">שגיאה בהצגת התראה, צור קשר עם המפתחים</string>
<string name="smp_proxy_error_connecting">שגיאה בהתחברות לשרת %1$s, אנא נסה מאוחר יותר</string>
<string name="message_forwarded_desc">אין עדיין חיבור ישיר, ההודעה תעובר ע\"י מנהל.</string>
<string name="message_forwarded_desc">אין עדיין חיבור ישיר, ההודעה תעובר ע"י מנהל.</string>
<string name="member_inactive_title">חבר לא פעיל</string>
<string name="xftp_servers_other">שרתי XFTP אחרים</string>
<string name="subscription_percentage">הראה אחוזים</string>
@@ -1920,11 +1920,11 @@
<string name="servers_info_files_tab">קבצים</string>
<string name="servers_info_missing">אין מידע, נסה לרענן</string>
<string name="servers_info">מידע על השרתים</string>
<string name="servers_info_detailed_statistics_received_total">התקבל סה\</string>
<string name="servers_info_detailed_statistics_received_total">התקבל סה"כ</string>
<string name="servers_info_detailed_statistics_receive_errors">התקבלו שגיאות</string>
<string name="reconnect">התחבר מחדש</string>
<string name="servers_info_detailed_statistics_sent_messages_header">שלח הודעות</string>
<string name="servers_info_detailed_statistics_sent_messages_total">נשלח בסה\</string>
<string name="servers_info_detailed_statistics_sent_messages_total">נשלח בסה"כ</string>
<string name="smp_server">שרת SMP</string>
<string name="xftp_server">שרת XFTP</string>
<string name="privacy_media_blur_radius_soft">חלש</string>
@@ -634,7 +634,7 @@
<string name="messages_section_title">메시지</string>
<string name="messages_section_description">이 설정은 현재 내 프로필의 메시지에 적용되어요.</string>
<string name="member_info_section_title_member">멤버</string>
<string name="member_role_will_be_changed_with_invitation">역할이 \"%s\"(으)로 변경되고, 회원은 새로운 초대를 받게 될 거예요.</string>
<string name="member_role_will_be_changed_with_invitation">역할이 "%s"(으)로 변경되고, 회원은 새로운 초대를 받게 될 거예요.</string>
<string name="message_deletion_prohibited">이 채팅에서는 메시지 영구 삭제가 허용되지 않았어요.</string>
<string name="leave_group_button">나가기</string>
<string name="large_file">큰 파일!</string>
@@ -675,11 +675,11 @@
<string name="leave_group_question">그룹에서 나갈까요\?</string>
<string name="mtr_error_no_down_migration">데이터베이스 버전이 앱보다 최신이지만, 다음에 대한 다운 마이그레이션 없음: %s</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">멤버가 그룹에서 제거되어요. 이 작업은 되돌릴 수 없어요!</string>
<string name="member_role_will_be_changed_with_notification">역할이 \"%s\"(으)로 변경되어요. 그룹의 모든 멤버에게 알림이 전송됩니다.</string>
<string name="member_role_will_be_changed_with_notification">역할이 "%s"(으)로 변경되어요. 그룹의 모든 멤버에게 알림이 전송됩니다.</string>
<string name="network_options_reset_to_defaults">기본값으로 재설정</string>
<string name="notification_preview_mode_message">메시지 내용</string>
<string name="notification_preview_mode_message_desc">대화 상대 이름 및 메시지 표시</string>
<string name="only_you_can_delete_messages">나만 메시지를 영구 삭제할 수 있어요(대화 상대는 \"삭제됨\" 표시만 할 수 있음).</string>
<string name="only_you_can_delete_messages">나만 메시지를 영구 삭제할 수 있어요(대화 상대는 "삭제됨" 표시만 할 수 있음).</string>
<string name="profile_will_be_sent_to_contact_sending_link">이 링크를 보낸 상대에게 프로필이 전송될 거예요.</string>
<string name="receiving_files_not_yet_supported">파일 수신은 아직 지원되지 않아요.</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">올바른 링크를 사용했는지 확인하거나 상대에게 다른 링크를 보내달라고 말해 주세요</string>
@@ -714,7 +714,7 @@
<string name="only_your_contact_can_send_voice">대화 상대만 음성 메시지를 보낼 수 있어요.</string>
<string name="prohibit_message_deletion">메시지 영구 삭제 허용되지 않음.</string>
<string name="prohibit_sending_voice">음성 메시지 허용되지 않음.</string>
<string name="only_your_contact_can_delete">상대만 메시지를 영구 삭제할 수 있어요(나는 \"삭제됨\"으로 표시만 할 수 있음).</string>
<string name="only_your_contact_can_delete">상대만 메시지를 영구 삭제할 수 있어요(나는 "삭제됨"으로 표시만 할 수 있음).</string>
<string name="only_group_owners_can_enable_voice">그룹 소유자만 음성 메시지를 사용 가능하도록 설정할 수 있어요.</string>
<string name="one_time_link">일회성 초대 링크</string>
<string name="paste_button">붙여넣기</string>
@@ -600,7 +600,7 @@
<string name="invite_prohibited">Nepavyko pakviesti kontakto!</string>
<string name="v4_5_transport_isolation_descr">Pagal pokalbių profilį (numatytieji nustatymai) arba pagal ryšį (BETA).</string>
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Naudoja daugiau baterijos</b>! Fono paslauga veikia visada - pranešimai rodomi, kai tik atsiranda žinučių.]]></string>
<string name="cannot_access_keychain">Negalima pasiekti \"Keystore\", kad išsaugotumėte duomenų bazės slaptažodį</string>
<string name="cannot_access_keychain">Negalima pasiekti "Keystore", kad išsaugotumėte duomenų bazės slaptažodį</string>
<string name="icon_descr_cancel_file_preview">Atšaukti failo peržiūrą</string>
<string name="icon_descr_cancel_image_preview">Atšaukti vaizdo peržiūrą</string>
<string name="share_text_database_id">Duomenų bazės ID: %d</string>
@@ -1484,7 +1484,7 @@
<string name="no_info_on_delivery">Nėra pristatymo informacijos</string>
<string name="v4_6_group_moderation_descr">Dabar administratoriai gali:
\n- ištrinti narių žinutes.
\n- išjungti narius (\"stebėtojas\" rolė)</string>
\n- išjungti narius ("stebėtojas" rolė)</string>
<string name="chat_preferences_on">įj.</string>
<string name="unfavorite_chat">Nebemėgti</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">Nustatymų atnaujinimas perjungs klientą iš naujo prie visų serverių.</string>
@@ -280,7 +280,7 @@
<string name="custom_time_unit_weeks">ആഴ്ചകൾ</string>
<string name="icon_descr_sent_msg_status_unauthorized_send">അനധികൃത അയക്കുക</string>
<string name="switch_receiving_address_question">സ്വീകരിക്കുന്ന വിലാസം മാറണോ\?</string>
<string name="member_role_will_be_changed_with_invitation">കര്‍ത്തവ്യം \"%s\" ആയി മാറ്റും. അംഗത്തിന് പുതിയ ക്ഷണം ലഭിക്കും.</string>
<string name="member_role_will_be_changed_with_invitation">കര്‍ത്തവ്യം "%s" ആയി മാറ്റും. അംഗത്തിന് പുതിയ ക്ഷണം ലഭിക്കും.</string>
<string name="la_lock_mode_system">സംവിധാനം പ്രാമാണീകരണം</string>
<string name="skip_inviting_button">അംഗങ്ങളെ ക്ഷണിക്കുന്നത് ഒഴിവാക്കുക</string>
<string name="save_welcome_message_question">സ്വാഗത സന്ദേശം സംരക്ഷിക്കണോ\?</string>
@@ -767,7 +767,7 @@
<string name="group_info_section_title_num_members">%1$s LEDEN</string>
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">U kunt een link of een QR-code delen. Iedereen kan lid worden van de groep. U verliest geen leden van de groep als u deze later verwijdert.</string>
<string name="switch_verb">Wijzig</string>
<string name="member_role_will_be_changed_with_notification">De rol wordt gewijzigd in \"%s\". Iedereen in de groep wordt op de hoogte gebracht.</string>
<string name="member_role_will_be_changed_with_notification">De rol wordt gewijzigd in "%s". Iedereen in de groep wordt op de hoogte gebracht.</string>
<string name="receiving_via">Ontvang via</string>
<string name="save_group_profile">Groep profiel opslaan</string>
<string name="group_is_decentralized">Volledig gedecentraliseerd alleen zichtbaar voor leden.</string>
@@ -800,7 +800,7 @@
<string name="button_remove_member">Lid verwijderen</string>
<string name="role_in_group">Rol</string>
<string name="button_send_direct_message">Direct bericht sturen</string>
<string name="member_role_will_be_changed_with_invitation">De rol wordt gewijzigd in \"%s\". De gebruiker ontvangt een nieuwe uitnodiging.</string>
<string name="member_role_will_be_changed_with_invitation">De rol wordt gewijzigd in "%s". De gebruiker ontvangt een nieuwe uitnodiging.</string>
<string name="sending_via">Verzenden via</string>
<string name="conn_stats_section_title_servers">SERVERS</string>
<string name="network_options_reset_to_defaults">Resetten naar standaardwaarden</string>
@@ -973,7 +973,7 @@
<string name="make_profile_private">Profiel privé maken!</string>
<string name="v4_6_group_moderation_descr">Nu kunnen beheerders:
\n- berichten van leden verwijderen.
\n- schakel leden uit (\"waarnemer\" rol)</string>
\n- schakel leden uit ("waarnemer" rol)</string>
<string name="v4_6_hidden_chat_profiles_descr">Bescherm je chat profielen met een wachtwoord!</string>
<string name="password_to_show">Wachtwoord om weer te geven</string>
<string name="save_and_update_group_profile">Groep profiel opslaan en bijwerken</string>
@@ -739,7 +739,7 @@
<string name="switch_verb">Przełącz</string>
<string name="switch_receiving_address">Zmień adres odbioru</string>
<string name="group_is_decentralized">W pełni zdecentralizowana widoczna tylko dla członków.</string>
<string name="member_role_will_be_changed_with_invitation">Rola zostanie zmieniona na \"%s\". Członek otrzyma nowe zaproszenie.</string>
<string name="member_role_will_be_changed_with_invitation">Rola zostanie zmieniona na "%s". Członek otrzyma nowe zaproszenie.</string>
<string name="group_welcome_title">Wiadomość powitalna</string>
<string name="users_delete_question">Usunąć profil czatu\?</string>
<string name="users_delete_profile_for">Usuń profil czatu dla</string>
@@ -944,7 +944,7 @@
<string name="failed_to_parse_chat_title">Nie udało się załadować czatu</string>
<string name="v4_6_group_moderation_descr">Teraz administratorzy mogą:
\n- usuwać wiadomości członków.
\n- wyłączyć członków (rola \"obserwatora\")</string>
\n- wyłączyć członków (rola "obserwatora")</string>
<string name="from_gallery_button">Z Galerii</string>
<string name="gallery_image_button">Obraz</string>
<string name="gallery_video_button">Wideo</string>
@@ -993,7 +993,7 @@
<string name="smp_servers_test_some_failed">Niektóre serwery nie przeszły testu:</string>
<string name="thank_you_for_installing_simplex">Dziękujemy za zainstalowanie SimpleX Chat!</string>
<string name="moderate_message_will_be_marked_warning">Wiadomość zostanie oznaczona jako zmoderowana dla wszystkich członków.</string>
<string name="member_role_will_be_changed_with_notification">Rola zostanie zmieniona na \"%s\". Wszyscy w grupie zostaną powiadomieni.</string>
<string name="member_role_will_be_changed_with_notification">Rola zostanie zmieniona na "%s". Wszyscy w grupie zostaną powiadomieni.</string>
<string name="delete_files_and_media_desc">Tego działania nie można cofnąć - wszystkie odebrane i wysłane pliki oraz media zostaną usunięte. Obrazy o niskiej rozdzielczości pozostaną.</string>
<string name="switch_receiving_address_desc">Adres odbiorczy zostanie zmieniony na inny serwer. Zmiana adresu zostanie zakończona gdy nadawca będzie online.</string>
<string name="this_link_is_not_a_valid_connection_link">Ten link nie jest prawidłowym linkiem połączenia!</string>
@@ -695,14 +695,14 @@
<string name="chat_preferences_no">não</string>
<string name="v4_6_group_moderation_descr">Agora administradores podem:
\n- excluir mensagens de membros.
\n- desativar membros (cargo de \"observador\")</string>
\n- desativar membros (cargo de "observador")</string>
<string name="v4_6_group_moderation">Moderação do grupo</string>
<string name="v4_6_group_welcome_message">Mensagem de boas-vindas do grupo</string>
<string name="database_downgrade">Desatualizar banco de dados</string>
<string name="mtr_error_different">migração diferente no aplicativo/banco de dados: %s / %s</string>
<string name="invite_to_group_button">Convidar para o grupo</string>
<string name="no_contacts_to_add">Sem contatos para adicionar</string>
<string name="member_role_will_be_changed_with_notification">O cargo será alterada para \"%s\". Todos no grupo serão notificados.</string>
<string name="member_role_will_be_changed_with_notification">O cargo será alterada para "%s". Todos no grupo serão notificados.</string>
<string name="user_mute">Mutar</string>
<string name="only_you_can_send_voice">Somente você pode enviar mensagens de voz.</string>
<string name="only_your_contact_can_send_voice">Somente seu contato pode enviar mensagens de voz.</string>
@@ -813,7 +813,7 @@
<string name="settings_section_title_experimenta">EXPERIMENTAL</string>
<string name="snd_conn_event_switch_queue_phase_completed">você alterou o endereço</string>
<string name="database_upgrade">Atualização do banco de dados</string>
<string name="member_role_will_be_changed_with_invitation">O cargo será alterado para \"%s\". O membro receberá um novo convite.</string>
<string name="member_role_will_be_changed_with_invitation">O cargo será alterado para "%s". O membro receberá um novo convite.</string>
<string name="only_group_owners_can_change_prefs">Somente os proprietários do grupo podem alterar as preferências do grupo.</string>
<string name="button_add_welcome_message">Adicionar mensagem de boas-vindas</string>
<string name="group_welcome_title">Mensagem de boas-vindas</string>
@@ -631,7 +631,7 @@
<string name="chat_preferences_on">ligado</string>
<string name="v4_6_group_moderation_descr">Agora os administradores podem:
\n- eliminar mensagens de membros.
\n- desativar membros (função de \"observador\")</string>
\n- desativar membros (função de "observador")</string>
<string name="simplex_link_group">Ligação do grupo SimpleX</string>
<string name="images_limit_desc">Apenas 10 imagens podem ser enviadas ao mesmo tempo</string>
<string name="videos_limit_desc">Apenas 10 vídeos podem ser enviados ao mesmo tempo</string>
@@ -796,8 +796,8 @@
<string name="change_verb">Поменять</string>
<string name="switch_verb">Переключить</string>
<string name="change_member_role_question">Поменять роль в группе?</string>
<string name="member_role_will_be_changed_with_notification">Роль будет изменена на \"%s\". Все в группе получат сообщение.</string>
<string name="member_role_will_be_changed_with_invitation">Роль будет изменена на \"%s\". Будет отправлено новое приглашение.</string>
<string name="member_role_will_be_changed_with_notification">Роль будет изменена на "%s". Все в группе получат сообщение.</string>
<string name="member_role_will_be_changed_with_invitation">Роль будет изменена на "%s". Будет отправлено новое приглашение.</string>
<string name="error_removing_member">Ошибка при удалении члена группы</string>
<string name="error_changing_role">Ошибка при изменении роли</string>
<string name="info_row_group">Группа</string>
@@ -964,7 +964,7 @@
<string name="v4_4_disappearing_messages">Исчезающие сообщения</string>
<string name="v4_4_disappearing_messages_desc">Отправленные сообщения будут удалены через заданное время.</string>
<string name="v4_3_improved_server_configuration">Улучшенная конфигурация серверов</string>
<string name="v4_4_live_messages">\"Живые\" сообщения</string>
<string name="v4_4_live_messages">"Живые" сообщения</string>
<string name="v4_4_live_messages_desc">Получатели видят их в то время как Вы их набираете.</string>
<string name="v4_4_verify_connection_security">Проверить безопасность соединения</string>
<string name="v4_4_verify_connection_security_desc">Сравните код безопасности с Вашими контактами.</string>
@@ -1065,7 +1065,7 @@
<string name="v4_6_reduced_battery_usage_descr">Дополнительные улучшения скоро!</string>
<string name="v4_6_group_moderation_descr">Теперь админы могут:
\n- удалять сообщения членов.
\n- приостанавливать членов (роль \"наблюдатель\")</string>
\n- приостанавливать членов (роль "наблюдатель")</string>
<string name="v4_6_hidden_chat_profiles_descr">Защитите Ваши профили чата паролем!</string>
<string name="user_unhide">Раскрыть</string>
<string name="v4_6_audio_video_calls_descr">Поддержка bluetooth и другие улучшения.</string>
@@ -742,7 +742,7 @@
<string name="v4_6_reduced_battery_usage_descr">การปรับปรุงเพิ่มเติมกำลังจะมาเร็ว ๆ นี้!</string>
<string name="v4_6_group_moderation_descr">ขณะนี้ผู้ดูแลระบบสามารถ:
\n- ลบข้อความของสมาชิก
\n- ปิดการใช้งานสมาชิก (บทบาท \"ผู้สังเกตการณ์\")</string>
\n- ปิดการใช้งานสมาชิก (บทบาท "ผู้สังเกตการณ์")</string>
<string name="v5_1_message_reactions">ปฏิกิริยาต่อข้อความ</string>
<string name="custom_time_unit_minutes">นาที</string>
<string name="custom_time_unit_months">เดือน</string>
@@ -1020,7 +1020,7 @@
<string name="v5_0_polish_interface_descr">ขอบคุณผู้ใช้ – มีส่วนร่วมผ่าน Weblate!</string>
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">แพลตฟอร์มการส่งข้อความและแอปพลิเคชันที่ปกป้องความเป็นส่วนตัวและความปลอดภัยของคุณ</string>
<string name="next_generation_of_private_messaging">การส่งข้อความส่วนตัวรุ่นต่อไป</string>
<string name="member_role_will_be_changed_with_notification">บทบาทจะถูกเปลี่ยนเป็น \"%s\" ทุกคนในกลุ่มจะได้รับแจ้ง</string>
<string name="member_role_will_be_changed_with_notification">บทบาทจะถูกเปลี่ยนเป็น "%s" ทุกคนในกลุ่มจะได้รับแจ้ง</string>
<string name="delete_files_and_media_desc">การดำเนินการนี้ไม่สามารถยกเลิกได้ ไฟล์และสื่อที่ได้รับและส่งทั้งหมดจะถูกลบ รูปภาพความละเอียดต่ำจะยังคงอยู่</string>
<string name="to_reveal_profile_enter_password">หากต้องการเปิดเผยโปรไฟล์ที่ซ่อนอยู่ของคุณ ให้ป้อนรหัสผ่านแบบเต็มในช่องค้นหาในหน้าโปรไฟล์แชทของคุณ</string>
<string name="network_session_mode_transport_isolation">การแยกการขนส่ง</string>
@@ -1198,7 +1198,7 @@
<string name="button_welcome_message">ข้อความต้อนรับ</string>
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">คุณสามารถแชร์ลิงก์หรือคิวอาร์โค้ดได้ ทุกคนจะสามารถเข้าร่วมกลุ่มได้ คุณจะไม่สูญเสียสมาชิกของกลุ่มหากคุณลบในภายหลัง</string>
<string name="you_can_share_this_address_with_your_contacts">คุณสามารถแบ่งปันที่อยู่นี้กับผู้ติดต่อของคุณเพื่อให้พวกเขาเชื่อมต่อกับ %s</string>
<string name="member_role_will_be_changed_with_invitation">บทบาทจะถูกเปลี่ยนเป็น \"%s\" สมาชิกจะได้รับคำเชิญใหม่</string>
<string name="member_role_will_be_changed_with_invitation">บทบาทจะถูกเปลี่ยนเป็น "%s" สมาชิกจะได้รับคำเชิญใหม่</string>
<string name="group_welcome_title">ข้อความต้อนรับ</string>
<string name="group_main_profile_sent">โปรไฟล์การแชทของคุณจะถูกส่งไปยังสมาชิกในกลุ่ม</string>
<string name="update_network_settings_confirmation">อัปเดต</string>
@@ -138,14 +138,14 @@
<string name="save_and_notify_contact">Kaydet ve kişiyi bilgilendir</string>
<string name="save_passphrase_in_keychain">Parolayı, Keystore\'a kaydet.</string>
<string name="icon_descr_audio_on">Ses açık</string>
<string name="member_role_will_be_changed_with_notification">Yetki, \"%s\" olarak değiştirelecek. Gruptaki herkes bilgilendirilecek.</string>
<string name="member_role_will_be_changed_with_notification">Yetki, "%s" olarak değiştirelecek. Gruptaki herkes bilgilendirilecek.</string>
<string name="calls_prohibited_with_this_contact">Sesli/görüntülü aramalar yasaktır.</string>
<string name="la_auth_failed">Kimlik doğrulama başarısız</string>
<string name="icon_descr_address">SimpleX Adresi</string>
<string name="moderate_message_will_be_deleted_warning">Mesaj, tüm üyeler için silinecek.</string>
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Gizliliğinizi ve güvenliğinizi koruyan mesajlaşma ve uygulama platformu.</string>
<string name="settings_section_title_incognito">Gizlilik kipi</string>
<string name="member_role_will_be_changed_with_invitation">Yetki, \"%s\" olarak değiştirilecek. Üye, yeni bir davet alacak.</string>
<string name="member_role_will_be_changed_with_invitation">Yetki, "%s" olarak değiştirilecek. Üye, yeni bir davet alacak.</string>
<string name="role_in_group">Yetki</string>
<string name="allow_voice_messages_question">Sesli mesajlara izin verilsin mi?</string>
<string name="users_add">Profil ekle</string>
@@ -360,7 +360,7 @@
<string name="ttl_hours">%d saat</string>
<string name="v4_6_group_moderation_descr">Yeni yöneticiler artık:
\n- üyelerin mesajlarını silebilir.
\n- üyeleri etkisizleştirebilir (\"gözlemci\" yetkisi verir)</string>
\n- üyeleri etkisizleştirebilir ("gözlemci" yetkisi verir)</string>
<string name="receipts_section_contacts">Konuşmalar</string>
<string name="create_group_link">Grup bağlantısı oluştur</string>
<string name="error_accepting_contact_request">Konuşma isteğini onaylarken hata oluştu</string>
@@ -726,8 +726,8 @@
<string name="share_text_disappears_at">Зникає о: %s</string>
<string name="item_info_current">(поточне)</string>
<string name="button_remove_member">Вилучити учасника</string>
<string name="member_role_will_be_changed_with_notification">Роль буде змінено на \"%s\". Всі учасники групи будуть сповіщені.</string>
<string name="member_role_will_be_changed_with_invitation">Роль буде змінено на \"%s\". Учасник отримає нове запрошення.</string>
<string name="member_role_will_be_changed_with_notification">Роль буде змінено на "%s". Всі учасники групи будуть сповіщені.</string>
<string name="member_role_will_be_changed_with_invitation">Роль буде змінено на "%s". Учасник отримає нове запрошення.</string>
<string name="info_row_group">Група</string>
<string name="group_welcome_title">Ласкаво просимо</string>
<string name="group_profile_is_stored_on_members_devices">Профіль групи зберігається на пристроях учасників, а не на серверах.</string>
@@ -1327,7 +1327,7 @@
<string name="enable_receipts_all">启用</string>
<string name="send_receipts_disabled_alert_msg">该群组成员超过 %1$d ,未发送送达回执。</string>
<string name="fix_connection_question">修复连接?</string>
<string name="v5_2_message_delivery_receipts_descr">我们错过的第二个\"√\"!✅</string>
<string name="v5_2_message_delivery_receipts_descr">我们错过的第二个"√"!✅</string>
<string name="setup_database_passphrase">设定数据库密码</string>
<string name="receipts_groups_title_disable">为群组禁用回执吗?</string>
<string name="rcv_group_event_3_members_connected">%s、%s 和 %s 已连接</string>
@@ -599,8 +599,8 @@
<string name="button_create_group_link">建立連結</string>
<string name="delete_link_question">刪除連結?</string>
<string name="button_remove_member">移除成員</string>
<string name="member_role_will_be_changed_with_notification">成員的身份會修改為 \"%s\"。所有在群組內的成員都接收到通知。</string>
<string name="member_role_will_be_changed_with_invitation">成員的身份會修改為 \"%s\"。該成員將接收到新的邀請。</string>
<string name="member_role_will_be_changed_with_notification">成員的身份會修改為 "%s"。所有在群組內的成員都接收到通知。</string>
<string name="member_role_will_be_changed_with_invitation">成員的身份會修改為 "%s"。該成員將接收到新的邀請。</string>
<string name="network_status">網路狀態</string>
<string name="network_options_reset_to_defaults">重置為預設值</string>
<string name="incognito_info_share">當你與某人分享已啟用匿名聊天模式的個人檔案時,此個人檔案將用於他們邀請你參加的群組。</string>
@@ -34,6 +34,9 @@ var useWorker = false;
var isDesktop = false;
var localizedState = "";
var localizedDescription = "";
// When one side of a call sends candidates tot fast (until local & remote descriptions are set), that candidates
// will be stored here and then set when the call will be ready to process them
let afterCallInitializedCandidates = [];
const processCommand = (function () {
const defaultIceServers = [
{ urls: ["stuns:stun.simplex.im:443"] },
@@ -234,6 +237,8 @@ const processCommand = (function () {
const pc = activeCall.connection;
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
addIceCandidates(pc, afterCallInitializedCandidates);
afterCallInitializedCandidates = [];
// for debugging, returning the command for callee to use
// resp = {
// type: "offer",
@@ -272,6 +277,8 @@ const processCommand = (function () {
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
addIceCandidates(pc, remoteIceCandidates);
addIceCandidates(pc, afterCallInitializedCandidates);
afterCallInitializedCandidates = [];
// same as command for caller to use
resp = {
type: "answer",
@@ -297,17 +304,20 @@ const processCommand = (function () {
// console.log("answer remoteIceCandidates", JSON.stringify(remoteIceCandidates))
await pc.setRemoteDescription(new RTCSessionDescription(answer));
addIceCandidates(pc, remoteIceCandidates);
addIceCandidates(pc, afterCallInitializedCandidates);
afterCallInitializedCandidates = [];
resp = { type: "ok" };
}
break;
case "ice":
const remoteIceCandidates = parse(command.iceCandidates);
if (pc) {
const remoteIceCandidates = parse(command.iceCandidates);
addIceCandidates(pc, remoteIceCandidates);
resp = { type: "ok" };
}
else {
resp = { type: "error", message: "ice: call not started" };
afterCallInitializedCandidates.push(...remoteIceCandidates);
resp = { type: "error", message: "ice: call not started yet, will add candidates later" };
}
break;
case "media":
@@ -18,7 +18,6 @@ kotlin {
dependencies {
implementation(project(":common"))
implementation(compose.desktop.currentOs)
implementation("net.java.dev.jna:jna:5.14.0")
}
}
val jvmTest by getting
+2 -2
View File
@@ -23,7 +23,7 @@ import Simplex.Chat
import Simplex.Chat.Controller
import Simplex.Chat.Core
import Simplex.Chat.Options
import Simplex.Messaging.Transport.Server (runTCPServer)
import Simplex.Messaging.Transport.Server (runLocalTCPServer)
import Simplex.Messaging.Util (raceAny_)
import UnliftIO.Exception
import UnliftIO.STM
@@ -68,7 +68,7 @@ newChatServerClient qSize = do
runChatServer :: ChatServerConfig -> ChatController -> IO ()
runChatServer ChatServerConfig {chatPort, clientQSize} cc = do
started <- newEmptyTMVarIO
runTCPServer started chatPort $ \sock -> do
runLocalTCPServer started chatPort $ \sock -> do
ws <- liftIO $ getConnection sock
c <- atomically $ newChatServerClient clientQSize
putStrLn "client connected"
@@ -66,7 +66,7 @@ This service continues running when the app is switched off, and it is restarted
So, for Android we can now deliver instant message notifications without compromising users' privacy in any way. The app version 1.5 that includes private instant notifications is now available on [Play Store](https://play.google.com/store/apps/details?id=chat.simplex.app), in our [F-Droid repo](https://app.simplex.chat/) and via direct [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) downloads!
Please let us what needs to be improved - it's only the first version of instant notifications for Android!
Please let us know what needs to be improved - it's only the first version of instant notifications for Android!
## Our iOS approach has one trade-off
@@ -96,8 +96,9 @@ The diagram below shows all the encryption layers used in private message routin
For private routing to work, both the forwardig and the destination relays should support the updated messaging protocol - it is supported from v5.8 of the messaging relays. It is already released to all relays preset in the app, and available as a self-hosted server. We updated [the guide](../docs/SERVER.md) about how to host your own messaging relays.
Because many self-hosted relays did not upgrade yet, private routing is not enabled by default. To enable it, you can open *Network & servers* settings in the app and change the settings in *Private message routing* section. We recommend setting *Private routing* option to *Unprotected* (to use it only with unknown relays and when not connecting via Tor) and *Allow downgrade* to *Yes* (so messages can still be delivered to the messaging relays that didn't upgrade yet) or to *When IP hidden* (in which case the messages will fail to deliver to unknown relays that didn't upgrade yet unless you connect to them via Tor).
See [F.A.Q. section](../docs/FAQ.md#does-simplex-protect-my-ip-address) for answers about private message routing.
Read more about the technical design of the private message routing in [this document](https://github.com/simplex-chat/simplexmq/blob/stable/rfcs/2023-09-12-second-relays.md).
Read more about the technical design of the private message routing in [this document](https://github.com/simplex-chat/simplexmq/blob/stable/rfcs/done/2023-09-12-second-relays.md) and in [the messaging protocol specification](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md#proxying-sender-commands).
## Server transparency
+1 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: d559a66145cf7b4cd367c09974ed1ce8393940b2
tag: 4268b90763c58358809a3ea7dd8bc7d78eeb3077
source-repository-package
type: git
+86 -5
View File
@@ -15,7 +15,9 @@ revision: 23.04.2024
- [How to configure and delete groups?](#how-to-configure-and-delete-groups)
- [Are there any reactions to messages? Can I answer specific messages directly?](#are-there-any-reactions-to-messages-can-i-answer-specific-messages-directly)
- [What do checkmarks mean?](#what-do-checkmarks-mean)
- [I want to see when my contacts read my messages](#i-want-to-see-when-my-contacts-read-my-messages)
- [Can I use the same profile on desktop? Do messages sync cross-platform?](#can-i-use-the-same-profile-on-desktop-do-messages-sync-cross-platform)
- [Why cannot I delete messages I sent from my contact's device?](#why-cannot-i-delete-messages-i-sent-from-my-contacts-device)
[Troubleshooting](#troubleshooting)
- [I do not receive messages or message notifications](#i-do-not-receive-messages-or-message-notifications)
@@ -30,6 +32,8 @@ revision: 23.04.2024
- [Does SimpleX support post quantum cryptography?](#does-simplex-support-post-quantum-cryptography)
- [What user data can be provided on request?](#what-user-data-can-be-provided-on-request)
- [Does SimpleX protect my IP address?](#does-simplex-protect-my-ip-address)
- [Doesn't private message routing reinvent Tor?](#doesnt-private-message-routing-reinvent-tor)
- [Why don't you embed Tor in SimpleX Chat app?](#why-dont-you-embed-tor-in-simplex-chat-app)
- [Can I host my own relays?](#can-i-host-my-own-relays)
[Funding and business model](#funding-and-business-model)
@@ -79,12 +83,43 @@ It's quite simple:
- two checkmarks - message is delivered to the recipient's device.
"sent" means accepted by the relay for delivery, "delivered" - stored on the recipient device.
Also see [ ](#i-do-not-see-the-second-tick-on-the-messages-i-sent)
Also see: [I do not see the second tick on the messages I sent](#i-do-not-see-the-second-tick-on-the-messages-i-sent)
### I want to see when my contacts read my messages
To know when your contact read your messages, your contact's app has to send you a confirmation message. And vice versa, for your contact to know when you read the message, your app has to send a confirmation message.
The important questions for this feature:
- do you always want that your contacts can see when you read all their messages? Probably, even with your close friends, sometimes you would prefer to have time before you answer their message, and also have a plausible deniability that you have not seen the message. And this should be ok - in the end, this is your device, and it should be for you to decide whether this confirmation message is sent or not, and when it is sent.
- what practical problems an automatic notification sent to your contacts when you read the message solves for you compared with you simply adding a reaction to a message or sending a quick reply?
Overall, it seems that this feature is more damaging to your communications with your contacts than it is helpful. It keeps senders longer in the app, nervously waiting for read receipts, exploiting addictive patterns - having you spend more time in the app is the reason why it is usually present in most messaging apps. It also creates a pressure on the recipients to reply sooner, and if read receipts are opt-in, it creates a pressure to enable it, that can be particularly damaging in any relationships with power imbalance.
We think that delivery receipts are important and equally benefit both sides as the conversation, as they confirm that communication network functions properly. But we strongly believe that read receipts is an anti-feature that only benefits the app developers, and hurts the relations between the app users. So we are not planning to add it even as opt-in. In case you want your contact to know you've read the message put a reaction to it. And if you don't want them to know it - it is also ok, what your device sends should be under your control.
### Can I use the same profile on desktop? Do messages sync cross-platform?
You can use your profile from mobile device on desktop. However, to do so you need to be on the same network, both on your mobile and desktop. More about it: [Release info](../blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md#link-mobile-and-desktop-apps-via-secure-quantum-resistant-protocol).
### Why cannot I delete messages I sent from my contact's device?
In SimpleX Chat, you and your contacts can delete the messages you send from recipients' devices if you both agree to that within 24 hours of sending it. To be able to do that you both have to enable "Delete for everyone" option in Contact preferences - tap on the contact's name above the conversation to get there.
You can also revoke the files you send. If the recipients did not yet receive the file, they will not be able to receive it after the file is revoked.
This is different from most other messengers that allow deleting messages from the recipients' devices without any agreement with the recipients.
We believe that allowing deleting information from your device to your contacts is a very wrong design decision for several reasons:
1) it violates your data sovereignty as the device owner - once your are in possession of any information, you have the rights to retain it, and any deletion should be agreed with you. And security and privacy is not possible if users don't have sovereignty over their devices.
2) it may be a business communication, and either your organization policy or a compliance requirement is that every message you receive must be preserved for some time.
3) the message can contain a legally binding promise, effectively a contract between you and your contact, in which case you both need to keep it.
4) the messages may contain threat or abuse and you may want to keep them as a proof.
5) you may have paid for the the message (e.g., it can be a design project or consulting report), and you don't want it to suddenly disappear before you had a chance to store it outside of the conversation.
It is also important to remember, that even if your contact enabled "Delete for everyone", you cannot really see it as a strong guarantee that the message will be deleted. Your contact's app can have a very simple modification (a one-line code change), that would prevent this deletion from happening when you request it. So you cannot see it as something that guarantees your security from your contacts.
When "Delete for everyone" is not enabled, you can still mark the sent message as deleted within 24 hours of sending it. In this case the recipient will see it as "deleted message", and will be able to reveal the original message.
## Troubleshooting
### I do not receive messages or message notifications
@@ -175,7 +210,7 @@ To determine whether it is the limitation of your, your contact's or both device
- if it is shown on your screen as soon as you start the call, then your device does not support call encryption.
- if in the beginning of the call your device shows "e2e encryption" but when your contact accepts the call it changes to "no e2e encryption", then it is only your contact's device that does not support it.
You need to upgrade webview (some Android systems allow it), Android system or the device to have support for e2e encryption in the calls - all modern webviews (and browsers) support it.
You need to upgrade webview (some Android systems allow it), Android system or the device to have support for e2e encryption in the calls - all modern WebViews (and browsers) support it.
### I clicked the link to connect, but could not connect
@@ -199,9 +234,55 @@ Please see our [Privacy Policy](../PRIVACY.md) and [Transparency Reports](./TRAN
### Does SimpleX protect my IP address?
Not fully yet, it is a work in progress. While your device does not connect to your contacts' devices directly, as it happens in p2p networks, your contacts can self-host their relays, and you will connect to them when sending messages. A modified relay can record IP addresses connecting devices, as is the case with any other server, including Tor entry nodes, VPN providers, etc. - IP address is fundamental to Internet functioning, and there will always be some server that can observe your IP address.
Yes!
We are currently working on the next version of message routing protocol that will protect your IP address from the relays chosen by your contacts, so it will only be visible to the relays chosen by you. Read about technical details here: [RFC](https://github.com/simplex-chat/simplexmq/blob/stable/rfcs/2023-09-12-second-relays.md).
SimpleX Chat from version 6.0 uses *private message routing* whenever you send messages to unknown servers (all servers in app network settings, both enabled and not, are considered "known").
For private routing to work, the servers chosen by your contacts (and by the group members in your groups) must be upgraded to the recent versions. Messaging servers include support for private routing from v5.8, but we recommend using the latest versions.
If the servers didn't upgrade, the messages would temporarily fail to deliver. You will see an orange warning icon on the message, and you can decide if you want to deliver them by connecting to these servers directly (it would require changing network settings). At the time of writing (August 2024), all preset servers and absolute majority of self-hosted servers we can see on the network support private message routing.
With private routing enabled, instead of connecting to your contact's server directly, your client would "instruct" one of the known servers to forward the message, preventing the destination server from observing your IP address.
Your messages are additionally end-to-end encrypted between your client and the destination server, so that the forwarding server cannot observe the destination addresses and server responses similarly to how onion routing work. Private message routing is, effectively, a two-hop onion packet routing.
Also, this connection is protected from man-in-the-middle attack by the forwarding server, as your client will validate destination server certificate using its fingerprint in the server address.
You can optionally enable private message routing for all servers in Advanced network settings to complicate traffic correlation for known servers too. This will be default once the clients are improved to "know about" and to take into account network server operators.
See [this post](../blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md#private-message-routing) for more details about how private message routing works.
### Doesn't private message routing reinvent Tor?
No, it does not!
It provides better privacy for messaging than Tor, and it can be used with and without Tor or other means to additionally protect your traffic from known servers as well.
Tor, VPN and other transport overlay networks route sockets, by creating long-lived TCP circuits between you and the destination server. While it protects your IP address, it does not protect your activity within this circuit. E.g., if you visit a website via Tor, it can still observe all pages you view within a session. Likewise, if you were connecting directly to a messaging server via Tor, this server would be able to list all message queues you send messages to.
Private message routing routes packets (each message is one 16kb packet), not sockets. Unlike Tor and VPN, it does not create circuits between your client and destination servers. The forwarding server creates one shared session between itself and the destination, and forwards all messages from you and other clients to that destination server, mixing messages from many clients into a single TCP session.
As each message uses its own random encryption key and random (non-sequential) identifier, the destination server cannot link multiple message queue addresses to the same client. At the same time, the forwarding server cannot observe which (and how many) addresses on the destination server your client sends messages to, thanks to e2e encryption between the client and destination server. In that regard, this design is similar to onion routing, but with per-packet anonymity, not per-circuit.
This design is similar to mixnets (e.g. [Nym network](https://nymtech.net)), and it is tailored to the needs of message routing, providing better transport anonymity that general purpose networks, like Tor or VPN. You still can use Tor or VPN to connect to known servers, to protect your IP address from them.
### Why don't you embed Tor in SimpleX Chat app?
[Tor](https://www.torproject.org) is a fantastic transport overlay network - we believe it might be the best there is right now. If its [threat model](https://support.torproject.org/about/attacks-on-onion-routing/) works for you, you absolutely should use it - SimpleX Chat app supports Tor via SOCKS proxy [since v3.1](https://simplex.chat/blog/20220808-simplex-chat-v3.1-chat-groups.html#access-messaging-servers-via-tor), and SimpleX network servers can be available on both public and onion address at the same time [since v3.2](https://simplex.chat/blog/20220901-simplex-chat-v3.2-incognito-mode.html#using-onion-server-addresses-with-tor), improving anonymity of the users who use Tor.
If you host your messaging server on the onion address only, the users who don't use Tor would still be able to message you via private message routing - all preset servers are configured to forward messages to onion-only servers.
But there are many reasons not to embed Tor in the app:
- it increases response latency, error rate, and battery usage, and we believe that for most users enabling Tor by default would be a bad trade-off.
- it would require us regularly updating Tor library in the app, and your Tor integrity would depend on us you would be "putting too many eggs in one basket".
- some networks restrict Tor traffic, so the app UI would have to support advanced Tor configuration, diverting our limited resources from the core app features that benefit all users.
- some countries have legislative restrictions on Tor usage, so we would have to support multiple app versions, also increasing our costs and slowing down the progress.
The last, but not the least, it would create an unfair competitive advantage to Tor. We believe in competition, and we want our users to be able to choose which transport overlay network to use, based on what network threat model works best for them.
If you want to use Tor or any other overlay network, such as i2p, [Nym network](https://nymtech.net), [Katzenpost](https://katzenpost.network), etc., you need to research their limitations, because none of them provides absolute anonymity against all possible attackers.
And if after that research you decide to use Tor, it takes about 2 minutes to install and start [Orbot app](https://guardianproject.info/apps/org.torproject.android/). We believe that if it seems complex, then you *should not* be using Tor - it is an advanced technology that can only improve your privacy and anonymity if you understand its limitations and know how to configure it.
### Can I host my own relays?
@@ -211,7 +292,7 @@ Of course! Please check these tutorials: [SMP server](./SERVER.md) and [XFTP ser
### How are you funded?
SimpleX Chat Ltd is funded by private investors and venture capital. As an open-source project, it is also being generously supported by donations as well. Read [more details](../blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md#how-is-it-funded-and-what-is-the-business-model).
SimpleX Chat Ltd is funded by private investors and venture capital. As an open-source project, it is also being generously supported by donations as well. Read the posts [from 2023](../blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md#how-is-it-funded-and-what-is-the-business-model) and [from 2024](../blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md) for more details.
### Why VCs?
+1 -1
View File
@@ -1,5 +1,5 @@
name: simplex-chat
version: 6.0.4.0
version: 6.0.5.0
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme
+12 -2
View File
@@ -219,6 +219,9 @@ var useWorker = false
var isDesktop = false
var localizedState = ""
var localizedDescription = ""
// When one side of a call sends candidates tot fast (until local & remote descriptions are set), that candidates
// will be stored here and then set when the call will be ready to process them
let afterCallInitializedCandidates: RTCIceCandidateInit[] = []
const processCommand = (function () {
type RTCRtpSenderWithEncryption = RTCRtpSender & {
@@ -445,6 +448,8 @@ const processCommand = (function () {
const pc = activeCall.connection
const offer = await pc.createOffer()
await pc.setLocalDescription(offer)
addIceCandidates(pc, afterCallInitializedCandidates)
afterCallInitializedCandidates = []
// for debugging, returning the command for callee to use
// resp = {
// type: "offer",
@@ -481,6 +486,8 @@ const processCommand = (function () {
const answer = await pc.createAnswer()
await pc.setLocalDescription(answer)
addIceCandidates(pc, remoteIceCandidates)
addIceCandidates(pc, afterCallInitializedCandidates)
afterCallInitializedCandidates = []
// same as command for caller to use
resp = {
type: "answer",
@@ -503,16 +510,19 @@ const processCommand = (function () {
// console.log("answer remoteIceCandidates", JSON.stringify(remoteIceCandidates))
await pc.setRemoteDescription(new RTCSessionDescription(answer))
addIceCandidates(pc, remoteIceCandidates)
addIceCandidates(pc, afterCallInitializedCandidates)
afterCallInitializedCandidates = []
resp = {type: "ok"}
}
break
case "ice":
const remoteIceCandidates: RTCIceCandidateInit[] = parse(command.iceCandidates)
if (pc) {
const remoteIceCandidates: RTCIceCandidateInit[] = parse(command.iceCandidates)
addIceCandidates(pc, remoteIceCandidates)
resp = {type: "ok"}
} else {
resp = {type: "error", message: "ice: call not started"}
afterCallInitializedCandidates.push(...remoteIceCandidates)
resp = {type: "error", message: "ice: call not started yet, will add candidates later"}
}
break
case "media":
@@ -38,6 +38,41 @@
</description>
<releases>
<release version="6.0.4" date="2024-08-31">
<url type="details">https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html</url>
<description>
<p>New in v6.0.1-4:</p>
<ul>
<li>reduce traffic and battery usage on unstable networks.</li>
<li>only offer the next versions for update, based on SerVer ordering.</li>
<li>UI fixes.</li>
<li>reduce app memory usage and start time.</li>
<li>faster sending of files to groups.</li>
<li>fix rare delivery bug.</li>
</ul>
<p>New in v6.0:</p>
<p>New chat experience:</p>
<ul>
<li>connect to your friends faster.</li>
<li>archive contacts to chat later.</li>
<li>delete up to 20 messages at once.</li>
<li>increase font size.</li>
</ul>
<p>New media options:</p>
<ul>
<li>play from the chat list.</li>
<li>blur for better privacy.</li>
</ul>
<p>Private routing:</p>
<ul>
<li>it protects your IP address and connections and is now enabled by default.</li>
</ul>
<p>Connection and servers information:</p>
<ul>
<li>to control your network status and usage.</li>
</ul>
</description>
</release>
<release version="6.0.3" date="2024-08-24">
<url type="details">https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html</url>
<description>
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."d559a66145cf7b4cd367c09974ed1ce8393940b2" = "1jav7jmriims6vlkxg8gmal03f9mbgrwc8v6g0rp95ivkx8gfjyw";
"https://github.com/simplex-chat/simplexmq.git"."4268b90763c58358809a3ea7dd8bc7d78eeb3077" = "0w444jbxxi5hgipf35xniwmffnpg4qb46sz112hrwxyf5syfi2k5";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
+1 -1
View File
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplex-chat
version: 6.0.4.0
version: 6.0.5.0
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat
+13 -8
View File
@@ -105,7 +105,7 @@ import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), Migrati
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
import Simplex.Messaging.Client (NetworkConfig (..), ProxyClientError (..), SocksMode (SMAlways), defaultNetworkConfig)
import Simplex.Messaging.Client (NetworkConfig (..), ProxyClientError (..), SocksMode (SMAlways), defaultNetworkConfig, textToHostMode)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
import qualified Simplex.Messaging.Crypto.File as CF
@@ -114,12 +114,12 @@ import qualified Simplex.Messaging.Crypto.Ratchet as CR
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (base64P)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport (TransportError (..))
import Simplex.Messaging.Transport.Client (defaultSocksProxy)
import Simplex.Messaging.Transport.Client (defaultSocksProxyWithAuth)
import Simplex.Messaging.Util
import Simplex.Messaging.Version
import Simplex.RemoteControl.Invitation (RCInvitation (..), RCSignedInvitation (..))
@@ -341,11 +341,11 @@ newChatController
userServers user' = useServers config protocol <$> withTransaction chatStore (`getProtocolServers` user')
updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig
updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_, tcpTimeout_, logTLSErrors} =
updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, tcpTimeout_, logTLSErrors} =
let cfg1 = maybe cfg (\smpProxyMode -> cfg {smpProxyMode}) smpProxyMode_
cfg2 = maybe cfg1 (\smpProxyFallback -> cfg1 {smpProxyFallback}) smpProxyFallback_
cfg3 = maybe cfg2 (\tcpTimeout -> cfg2 {tcpTimeout, tcpConnectTimeout = (tcpTimeout * 3) `div` 2}) tcpTimeout_
in cfg3 {socksProxy, socksMode, logTLSErrors}
in cfg3 {socksProxy, socksMode, hostMode, requiredHostMode, logTLSErrors}
withChatLock :: String -> CM a -> CM a
withChatLock name action = asks chatLock >>= \l -> withLock l name action
@@ -2763,7 +2763,10 @@ processChatCommand' vr = \case
filesInfo <- withFastStore' (`getUserFileInfo` user)
cancelFilesInProgress user filesInfo
deleteFilesLocally filesInfo
withAgent $ \a -> deleteUser a (aUserId user) delSMPQueues
withAgent (\a -> deleteUser a (aUserId user) delSMPQueues)
`catchChatError` \case
e@(ChatErrorAgent NO_USER _) -> toView $ CRChatError (Just user) e
e -> throwError e
withFastStore' (`deleteUserRecord` user)
when (activeUser user) $ chatWriteVar currentUser Nothing
ok_
@@ -8049,14 +8052,16 @@ chatCommandP =
<|> ("yes" $> TMEEnableKeepTTL)
<|> ("no" $> TMEDisableKeepTTL)
netCfgP = do
socksProxy <- "socks=" *> ("off" $> Nothing <|> "on" $> Just defaultSocksProxy <|> Just <$> strP)
socksProxy <- "socks=" *> ("off" $> Nothing <|> "on" $> Just defaultSocksProxyWithAuth <|> Just <$> strP)
socksMode <- " socks-mode=" *> strP <|> pure SMAlways
hostMode <- " host-mode=" *> (textToHostMode . safeDecodeUtf8 <$?> A.takeTill (== ' ')) <|> pure (defaultHostMode socksProxy)
requiredHostMode <- " required-host-mode" *> onOffP <|> pure False
smpProxyMode_ <- optional $ " smp-proxy=" *> strP
smpProxyFallback_ <- optional $ " smp-proxy-fallback=" *> strP
t_ <- optional $ " timeout=" *> A.decimal
logTLSErrors <- " log=" *> onOffP <|> pure False
let tcpTimeout_ = (1000000 *) <$> t_
pure $ SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_, tcpTimeout_, logTLSErrors}
pure $ SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, tcpTimeout_, logTLSErrors}
dbKeyP = nonEmptyKey <$?> strP
nonEmptyKey k@(DBEncryptionKey s) = if BA.null s then Left "empty key" else Right k
dbEncryptionConfig currentKey newKey = DBEncryptionConfig {currentKey, newKey, keepKey = Just False}
+6 -4
View File
@@ -76,7 +76,7 @@ import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction, withTransactionPriority)
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import Simplex.Messaging.Client (SMPProxyFallback (..), SMPProxyMode (..), SocksMode (..))
import Simplex.Messaging.Client (HostMode (..), SMPProxyFallback (..), SMPProxyMode (..), SocksMode (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..))
import qualified Simplex.Messaging.Crypto.File as CF
@@ -87,7 +87,7 @@ import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, p
import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, NtfServer, ProtocolType (..), ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol)
import Simplex.Messaging.TMap (TMap)
import Simplex.Messaging.Transport (TLS, simplexMQVersion)
import Simplex.Messaging.Transport.Client (SocksProxy, TransportHost)
import Simplex.Messaging.Transport.Client (SocksProxyWithAuth, TransportHost)
import Simplex.Messaging.Util (allFinally, catchAllErrors, catchAllErrors', tryAllErrors, tryAllErrors', (<$$>))
import Simplex.RemoteControl.Client
import Simplex.RemoteControl.Invitation (RCSignedInvitation, RCVerifiedInvitation)
@@ -974,8 +974,10 @@ data AppFilePathsConfig = AppFilePathsConfig
deriving (Show)
data SimpleNetCfg = SimpleNetCfg
{ socksProxy :: Maybe SocksProxy,
{ socksProxy :: Maybe SocksProxyWithAuth,
socksMode :: SocksMode,
hostMode :: HostMode,
requiredHostMode :: Bool,
smpProxyMode_ :: Maybe SMPProxyMode,
smpProxyFallback_ :: Maybe SMPProxyFallback,
tcpTimeout_ :: Maybe Int,
@@ -984,7 +986,7 @@ data SimpleNetCfg = SimpleNetCfg
deriving (Show)
defaultSimpleNetCfg :: SimpleNetCfg
defaultSimpleNetCfg = SimpleNetCfg Nothing SMAlways Nothing Nothing Nothing False
defaultSimpleNetCfg = SimpleNetCfg Nothing SMAlways HMOnionViaSocks True Nothing Nothing Nothing False
data ContactSubStatus = ContactSubStatus
{ contact :: Contact,
+37 -4
View File
@@ -13,6 +13,7 @@ module Simplex.Chat.Options
coreChatOptsP,
getChatOpts,
protocolServersP,
defaultHostMode,
)
where
@@ -20,6 +21,7 @@ import Control.Logger.Simple (LogLevel (..))
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.ByteArray (ScrubbedBytes)
import qualified Data.ByteString.Char8 as B
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
@@ -27,11 +29,11 @@ import Numeric.Natural (Natural)
import Options.Applicative
import Simplex.Chat.Controller (ChatLogLevel (..), SimpleNetCfg (..), updateStr, versionNumber, versionString)
import Simplex.FileTransfer.Description (mb)
import Simplex.Messaging.Client (SocksMode (..))
import Simplex.Messaging.Client (HostMode (..), SocksMode (..), textToHostMode)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI, SMPServerWithAuth, XFTPServerWithAuth)
import Simplex.Messaging.Transport.Client (defaultSocksProxy)
import Simplex.Messaging.Transport.Client (SocksProxyWithAuth (..), SocksAuth (..), defaultSocksProxyWithAuth)
import System.FilePath (combine)
data ChatOpts = ChatOpts
@@ -123,7 +125,7 @@ coreChatOptsP appDir defaultDbFileName = do
<> value []
)
socksProxy <-
flag' (Just defaultSocksProxy) (short 'x' <> help "Use local SOCKS5 proxy at :9050")
flag' (Just defaultSocksProxyWithAuth) (short 'x' <> help "Use local SOCKS5 proxy at :9050")
<|> option
strParse
( long "socks-proxy"
@@ -139,6 +141,19 @@ coreChatOptsP appDir defaultDbFileName = do
<> help "Use SOCKS5 proxy: always (default), onion (with onion-only relays)"
<> value SMAlways
)
hostMode_ <-
optional $
option
parseHostMode
( long "host-mode"
<> metavar "HOST_MODE"
<> help "Preferred server host type: onion (when SOCKS proxy with isolate-by-auth is used), public"
)
requiredHostMode <-
switch
( long "required-host-mode"
<> help "Refuse connection if preferred server host type is not available"
)
smpProxyMode_ <-
optional $
option
@@ -226,7 +241,17 @@ coreChatOptsP appDir defaultDbFileName = do
dbKey,
smpServers,
xftpServers,
simpleNetCfg = SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_, tcpTimeout_ = Just $ useTcpTimeout socksProxy t, logTLSErrors},
simpleNetCfg =
SimpleNetCfg
{ socksProxy,
socksMode,
hostMode = fromMaybe (defaultHostMode socksProxy) hostMode_,
requiredHostMode,
smpProxyMode_,
smpProxyFallback_,
tcpTimeout_ = Just $ useTcpTimeout socksProxy t,
logTLSErrors
},
logLevel,
logConnections = logConnections || logLevel <= CLLInfo,
logServerHosts = logServerHosts || logLevel <= CLLInfo,
@@ -240,6 +265,11 @@ coreChatOptsP appDir defaultDbFileName = do
useTcpTimeout p t = 1000000 * if t > 0 then t else maybe 7 (const 15) p
defaultDbFilePath = combine appDir defaultDbFileName
defaultHostMode :: Maybe SocksProxyWithAuth -> HostMode
defaultHostMode = \case
Just (SocksProxyWithAuth SocksIsolateByAuth _) -> HMOnionViaSocks;
_ -> HMPublic
chatOptsP :: FilePath -> FilePath -> Parser ChatOpts
chatOptsP appDir defaultDbFileName = do
coreOptions <- coreChatOptsP appDir defaultDbFileName
@@ -360,6 +390,9 @@ parseProtocolServers = eitherReader $ parseAll protocolServersP . B.pack
strParse :: StrEncoding a => ReadM a
strParse = eitherReader $ parseAll strP . encodeUtf8 . T.pack
parseHostMode :: ReadM HostMode
parseHostMode = eitherReader $ textToHostMode . T.pack
parseServerPort :: ReadM (Maybe String)
parseServerPort = eitherReader $ parseAll serverPortP . B.pack
+5 -5
View File
@@ -540,21 +540,21 @@ data ExtMsgContent = ExtMsgContent {content :: MsgContent, file :: Maybe FileInv
$(JQ.deriveJSON defaultJSON ''QuotedMsg)
-- this limit reserves space for metadata in forwarded messages
-- 15780 (limit used for fileChunkSize) - 161 (x.grp.msg.forward overhead) = 15619, round to 15610
-- 15780 (limit used for fileChunkSize) - 161 (x.grp.msg.forward overhead) = 15619, - 16 for block encryption ("rounded" to 15602)
maxEncodedMsgLength :: Int
maxEncodedMsgLength = 15610
maxEncodedMsgLength = 15602
-- maxEncodedMsgLength - 2222, see e2eEncUserMsgLength in agent
maxCompressedMsgLength :: Int
maxCompressedMsgLength = 13388
maxCompressedMsgLength = 13380
-- maxEncodedMsgLength - delta between MSG and INFO + 100 (returned for forward overhead)
-- delta between MSG and INFO = e2eEncUserMsgLength (no PQ) - e2eEncConnInfoLength (no PQ) = 1008
maxEncodedInfoLength :: Int
maxEncodedInfoLength = 14702
maxEncodedInfoLength = 14694
maxCompressedInfoLength :: Int
maxCompressedInfoLength = 10976 -- maxEncodedInfoLength - 3726, see e2eEncConnInfoLength in agent
maxCompressedInfoLength = 10968 -- maxEncodedInfoLength - 3726, see e2eEncConnInfoLength in agent
data EncodedChatMessage = ECMEncoded ByteString | ECMLarge
-1
View File
@@ -383,7 +383,6 @@ deleteUnusedIncognitoProfileById_ db User {userId} profileId =
|]
[":user_id" := userId, ":profile_id" := profileId]
type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. (Maybe GroupMemberId, Bool, Maybe UIThemeEntityOverrides, Bool, Maybe CustomData)
type ContactRow = Only ContactId :. ContactRow'