mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f730d54e9 | |||
| fe0013c4a9 | |||
| 5261886b31 | |||
| 93ab3076d4 | |||
| 3b88ddbd4f | |||
| d6dc35738e | |||
| 5b3aba9db2 | |||
| 981cbb8bf9 | |||
| 25fb099c44 | |||
| 40e93cc61e | |||
| c22d23750f | |||
| 2539255957 | |||
| 3ea8279451 | |||
| 0cb568d206 | |||
| 351cfcbcbc | |||
| 691cd489ea | |||
| bccbb9900f | |||
| e002f33c53 | |||
| 6407d5de63 | |||
| 9cba96082d | |||
| 2089fd8539 | |||
| 31c4ff2705 | |||
| fe20a43232 | |||
| 087519c39d | |||
| fff29f8548 | |||
| 8cc03b6c21 | |||
| 6adf8f29b0 | |||
| 4ca1b57e1b | |||
| 9432a5e5cd | |||
| a9ec1f9ec1 | |||
| 122387d180 | |||
| 6edea46dad | |||
| 2fe3acf4df | |||
| dfe16991d0 | |||
| acb372a4ce | |||
| 121eaf6073 | |||
| 76cb9013f5 | |||
| f1e8c65aa1 |
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -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> = [:]
|
||||
|
||||
@@ -2182,9 +2182,11 @@ func refreshCallInvitations() async throws {
|
||||
}
|
||||
}
|
||||
|
||||
func justRefreshCallInvitations() throws {
|
||||
func justRefreshCallInvitations() async throws {
|
||||
let callInvitations = try apiGetCallInvitationsSync()
|
||||
ChatModel.shared.callInvitations = callsByChat(callInvitations)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.callInvitations = callsByChat(callInvitations)
|
||||
}
|
||||
}
|
||||
|
||||
private func callsByChat(_ callInvitations: [RcvCallInvitation]) -> [ChatId: RcvCallInvitation] {
|
||||
@@ -2194,12 +2196,13 @@ private func callsByChat(_ callInvitations: [RcvCallInvitation]) -> [ChatId: Rcv
|
||||
}
|
||||
|
||||
func activateCall(_ callInvitation: RcvCallInvitation) {
|
||||
if !callInvitation.user.showNotifications { return }
|
||||
let m = ChatModel.shared
|
||||
logger.debug("reportNewIncomingCall activeCallUUID \(String(describing: m.activeCall?.callUUID)) invitationUUID \(String(describing: callInvitation.callUUID))")
|
||||
if !callInvitation.user.showNotifications || m.activeCall?.callUUID == callInvitation.callUUID { return }
|
||||
CallController.shared.reportNewIncomingCall(invitation: callInvitation) { error in
|
||||
if let error = error {
|
||||
DispatchQueue.main.async {
|
||||
m.callInvitations[callInvitation.contact.id]?.callkitUUID = nil
|
||||
m.callInvitations[callInvitation.contact.id]?.callUUID = nil
|
||||
}
|
||||
logger.error("reportNewIncomingCall error: \(error.localizedDescription)")
|
||||
} else {
|
||||
|
||||
@@ -185,7 +185,7 @@ struct ActiveCallView: View {
|
||||
case .ended:
|
||||
closeCallView(client)
|
||||
call.callState = .ended
|
||||
if let uuid = call.callkitUUID {
|
||||
if let uuid = call.callUUID {
|
||||
CallController.shared.endCall(callUUID: uuid)
|
||||
}
|
||||
case .ok:
|
||||
@@ -382,7 +382,7 @@ struct ActiveCallOverlay: View {
|
||||
private func endCallButton() -> some View {
|
||||
let cc = CallController.shared
|
||||
return callButton("phone.down.fill", width: 60, height: 60) {
|
||||
if let uuid = call.callkitUUID {
|
||||
if let uuid = call.callUUID {
|
||||
cc.endCall(callUUID: uuid)
|
||||
} else {
|
||||
cc.endCall(call: call) {}
|
||||
@@ -462,9 +462,9 @@ struct ActiveCallOverlay: View {
|
||||
struct ActiveCallOverlay_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group{
|
||||
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callkitUUID: UUID(), callState: .offerSent, localMedia: .video), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil)))
|
||||
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callUUID: UUID().uuidString.lowercased(), callState: .offerSent, localMedia: .video), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil)))
|
||||
.background(.black)
|
||||
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callkitUUID: UUID(), callState: .offerSent, localMedia: .audio), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil)))
|
||||
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callUUID: UUID().uuidString.lowercased(), callState: .offerSent, localMedia: .audio), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil)))
|
||||
.background(.black)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
|
||||
logger.debug("CallController.provider CXStartCallAction")
|
||||
if callManager.startOutgoingCall(callUUID: action.callUUID) {
|
||||
if callManager.startOutgoingCall(callUUID: action.callUUID.uuidString.lowercased()) {
|
||||
action.fulfill()
|
||||
provider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: nil)
|
||||
} else {
|
||||
@@ -61,12 +61,30 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
|
||||
logger.debug("CallController.provider CXAnswerCallAction")
|
||||
if callManager.answerIncomingCall(callUUID: action.callUUID) {
|
||||
// WebRTC call should be in connected state to fulfill.
|
||||
// Otherwise no audio and mic working on lockscreen
|
||||
fulfillOnConnect = action
|
||||
} else {
|
||||
action.fail()
|
||||
Task {
|
||||
let chatIsReady = await waitUntilChatStarted(timeoutMs: 30_000, stepMs: 500)
|
||||
logger.debug("CallController chat started \(chatIsReady) \(ChatModel.shared.chatInitialized) \(ChatModel.shared.chatRunning == true) \(String(describing: AppChatState.shared.value))")
|
||||
if !chatIsReady {
|
||||
action.fail()
|
||||
return
|
||||
}
|
||||
if !ChatModel.shared.callInvitations.values.contains(where: { inv in inv.callUUID == action.callUUID.uuidString.lowercased() }) {
|
||||
try? await justRefreshCallInvitations()
|
||||
logger.debug("CallController: updated call invitations chat")
|
||||
}
|
||||
await MainActor.run {
|
||||
logger.debug("CallController.provider will answer on call")
|
||||
|
||||
if callManager.answerIncomingCall(callUUID: action.callUUID.uuidString.lowercased()) {
|
||||
logger.debug("CallController.provider answered on call")
|
||||
// WebRTC call should be in connected state to fulfill.
|
||||
// Otherwise no audio and mic working on lockscreen
|
||||
fulfillOnConnect = action
|
||||
} else {
|
||||
logger.debug("CallController.provider will fail the call")
|
||||
action.fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +93,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
// Should be nil here if connection was in connected state
|
||||
fulfillOnConnect?.fail()
|
||||
fulfillOnConnect = nil
|
||||
callManager.endCall(callUUID: action.callUUID) { ok in
|
||||
callManager.endCall(callUUID: action.callUUID.uuidString.lowercased()) { ok in
|
||||
if ok {
|
||||
action.fulfill()
|
||||
} else {
|
||||
@@ -86,7 +104,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
|
||||
if callManager.enableMedia(media: .audio, enable: !action.isMuted, callUUID: action.callUUID) {
|
||||
if callManager.enableMedia(media: .audio, enable: !action.isMuted, callUUID: action.callUUID.uuidString.lowercased()) {
|
||||
action.fulfill()
|
||||
} else {
|
||||
action.fail()
|
||||
@@ -156,6 +174,19 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
}
|
||||
}
|
||||
|
||||
private func waitUntilChatStarted(timeoutMs: UInt64, stepMs: UInt64) async -> Bool {
|
||||
logger.debug("CallController waiting until chat started")
|
||||
var t: UInt64 = 0
|
||||
repeat {
|
||||
if ChatModel.shared.chatInitialized, ChatModel.shared.chatRunning == true, case .active = AppChatState.shared.value {
|
||||
return true
|
||||
}
|
||||
_ = try? await Task.sleep(nanoseconds: stepMs * 1000000)
|
||||
t += stepMs
|
||||
} while t < timeoutMs
|
||||
return false
|
||||
}
|
||||
|
||||
@objc(pushRegistry:didUpdatePushCredentials:forType:)
|
||||
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
|
||||
logger.debug("CallController: didUpdate push credentials for type \(type.rawValue)")
|
||||
@@ -171,32 +202,19 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
self.reportExpiredCall(payload: payload, completion)
|
||||
return
|
||||
}
|
||||
if (!ChatModel.shared.chatInitialized) {
|
||||
logger.debug("CallController: initializing chat")
|
||||
do {
|
||||
try initializeChat(start: true, refreshInvitations: false)
|
||||
} catch let error {
|
||||
logger.error("CallController: initializing chat error: \(error)")
|
||||
self.reportExpiredCall(payload: payload, completion)
|
||||
return
|
||||
}
|
||||
}
|
||||
logger.debug("CallController: initialized chat")
|
||||
startChatForCall()
|
||||
logger.debug("CallController: started chat")
|
||||
self.shouldSuspendChat = true
|
||||
// There are no invitations in the model, as it was processed by NSE
|
||||
try? justRefreshCallInvitations()
|
||||
logger.debug("CallController: updated call invitations chat")
|
||||
// logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))")
|
||||
// Extract the call information from the push notification payload
|
||||
let m = ChatModel.shared
|
||||
if let contactId = payload.dictionaryPayload["contactId"] as? String,
|
||||
let invitation = m.callInvitations[contactId] {
|
||||
let update = self.cxCallUpdate(invitation: invitation)
|
||||
if let uuid = invitation.callkitUUID {
|
||||
let displayName = payload.dictionaryPayload["displayName"] as? String,
|
||||
let callUUID = payload.dictionaryPayload["callUUID"] as? String,
|
||||
let uuid = UUID(uuidString: callUUID),
|
||||
let callTsInterval = payload.dictionaryPayload["callTs"] as? TimeInterval,
|
||||
let mediaStr = payload.dictionaryPayload["media"] as? String,
|
||||
let media = CallMediaType(rawValue: mediaStr) {
|
||||
let update = self.cxCallUpdate(contactId, displayName, media)
|
||||
let callTs = Date(timeIntervalSince1970: callTsInterval)
|
||||
if callTs.timeIntervalSinceNow >= -180 {
|
||||
logger.debug("CallController: report pushkit call via CallKit")
|
||||
let update = self.cxCallUpdate(invitation: invitation)
|
||||
self.provider.reportNewIncomingCall(with: uuid, update: update) { error in
|
||||
if error != nil {
|
||||
m.callInvitations.removeValue(forKey: contactId)
|
||||
@@ -205,11 +223,31 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
completion()
|
||||
}
|
||||
} else {
|
||||
logger.debug("CallController will expire call 1")
|
||||
self.reportExpiredCall(update: update, completion)
|
||||
}
|
||||
} else {
|
||||
logger.debug("CallController will expire call 2")
|
||||
self.reportExpiredCall(payload: payload, completion)
|
||||
}
|
||||
|
||||
//DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
|
||||
if (!ChatModel.shared.chatInitialized) {
|
||||
logger.debug("CallController: initializing chat")
|
||||
do {
|
||||
try initializeChat(start: true, refreshInvitations: false)
|
||||
} catch let error {
|
||||
logger.error("CallController: initializing chat error: \(error)")
|
||||
if let call = ChatModel.shared.activeCall {
|
||||
self.endCall(call: call, completed: completion)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
logger.debug("CallController: initialized chat")
|
||||
startChatForCall()
|
||||
logger.debug("CallController: started chat")
|
||||
self.shouldSuspendChat = true
|
||||
}
|
||||
|
||||
// This function fulfils the requirement to always report a call when PushKit notification is received,
|
||||
@@ -239,8 +277,8 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
}
|
||||
|
||||
func reportNewIncomingCall(invitation: RcvCallInvitation, completion: @escaping (Error?) -> Void) {
|
||||
logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callkitUUID))")
|
||||
if CallController.useCallKit(), let uuid = invitation.callkitUUID {
|
||||
logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callUUID))")
|
||||
if CallController.useCallKit(), let callUUID = invitation.callUUID, let uuid = UUID(uuidString: callUUID) {
|
||||
if invitation.callTs.timeIntervalSinceNow >= -180 {
|
||||
let update = cxCallUpdate(invitation: invitation)
|
||||
provider.reportNewIncomingCall(with: uuid, update: update, completion: completion)
|
||||
@@ -261,6 +299,14 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
return update
|
||||
}
|
||||
|
||||
private func cxCallUpdate(_ contactId: String, _ displayName: String, _ media: CallMediaType) -> CXCallUpdate {
|
||||
let update = CXCallUpdate()
|
||||
update.remoteHandle = CXHandle(type: .generic, value: contactId)
|
||||
update.hasVideo = media == .video
|
||||
update.localizedCallerName = displayName
|
||||
return update
|
||||
}
|
||||
|
||||
func reportIncomingCall(call: Call, connectedAt dateConnected: Date?) {
|
||||
logger.debug("CallController: reporting incoming call connected")
|
||||
if CallController.useCallKit() {
|
||||
@@ -272,14 +318,14 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
|
||||
func reportOutgoingCall(call: Call, connectedAt dateConnected: Date?) {
|
||||
logger.debug("CallController: reporting outgoing call connected")
|
||||
if CallController.useCallKit(), let uuid = call.callkitUUID {
|
||||
if CallController.useCallKit(), let callUUID = call.callUUID, let uuid = UUID(uuidString: callUUID) {
|
||||
provider.reportOutgoingCall(with: uuid, connectedAt: dateConnected)
|
||||
}
|
||||
}
|
||||
|
||||
func reportCallRemoteEnded(invitation: RcvCallInvitation) {
|
||||
logger.debug("CallController: reporting remote ended")
|
||||
if CallController.useCallKit(), let uuid = invitation.callkitUUID {
|
||||
if CallController.useCallKit(), let callUUID = invitation.callUUID, let uuid = UUID(uuidString: callUUID) {
|
||||
provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
|
||||
} else if invitation.contact.id == activeCallInvitation?.contact.id {
|
||||
activeCallInvitation = nil
|
||||
@@ -288,14 +334,17 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
|
||||
func reportCallRemoteEnded(call: Call) {
|
||||
logger.debug("CallController: reporting remote ended")
|
||||
if CallController.useCallKit(), let uuid = call.callkitUUID {
|
||||
if CallController.useCallKit(), let callUUID = call.callUUID, let uuid = UUID(uuidString: callUUID) {
|
||||
provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
|
||||
}
|
||||
}
|
||||
|
||||
func startCall(_ contact: Contact, _ media: CallMediaType) {
|
||||
logger.debug("CallController.startCall")
|
||||
let uuid = callManager.newOutgoingCall(contact, media)
|
||||
let callUUID = callManager.newOutgoingCall(contact, media)
|
||||
guard let uuid = UUID(uuidString: callUUID) else {
|
||||
return
|
||||
}
|
||||
if CallController.useCallKit() {
|
||||
let handle = CXHandle(type: .generic, value: contact.id)
|
||||
let action = CXStartCallAction(call: uuid, handle: handle)
|
||||
@@ -307,8 +356,8 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
update.localizedCallerName = contact.displayName
|
||||
self.provider.reportCall(with: uuid, updated: update)
|
||||
}
|
||||
} else if callManager.startOutgoingCall(callUUID: uuid) {
|
||||
if callManager.startOutgoingCall(callUUID: uuid) {
|
||||
} else if callManager.startOutgoingCall(callUUID: callUUID) {
|
||||
if callManager.startOutgoingCall(callUUID: callUUID) {
|
||||
logger.debug("CallController.startCall: call started")
|
||||
} else {
|
||||
logger.error("CallController.startCall: no active call")
|
||||
@@ -318,8 +367,8 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
|
||||
func answerCall(invitation: RcvCallInvitation) {
|
||||
logger.debug("CallController: answering a call")
|
||||
if CallController.useCallKit(), let callUUID = invitation.callkitUUID {
|
||||
requestTransaction(with: CXAnswerCallAction(call: callUUID))
|
||||
if CallController.useCallKit(), let callUUID = invitation.callUUID, let uuid = UUID(uuidString: callUUID) {
|
||||
requestTransaction(with: CXAnswerCallAction(call: uuid))
|
||||
} else {
|
||||
callManager.answerIncomingCall(invitation: invitation)
|
||||
}
|
||||
@@ -328,10 +377,13 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
}
|
||||
}
|
||||
|
||||
func endCall(callUUID: UUID) {
|
||||
logger.debug("CallController: ending the call with UUID \(callUUID.uuidString)")
|
||||
func endCall(callUUID: String) {
|
||||
let uuid = UUID(uuidString: callUUID)
|
||||
logger.debug("CallController: ending the call with UUID \(callUUID)")
|
||||
if CallController.useCallKit() {
|
||||
requestTransaction(with: CXEndCallAction(call: callUUID))
|
||||
if let uuid {
|
||||
requestTransaction(with: CXEndCallAction(call: uuid))
|
||||
}
|
||||
} else {
|
||||
callManager.endCall(callUUID: callUUID) { ok in
|
||||
if ok {
|
||||
|
||||
@@ -10,17 +10,17 @@ import Foundation
|
||||
import SimpleXChat
|
||||
|
||||
class CallManager {
|
||||
func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> UUID {
|
||||
let uuid = UUID()
|
||||
let call = Call(direction: .outgoing, contact: contact, callkitUUID: uuid, callState: .waitCapabilities, localMedia: media)
|
||||
func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> String {
|
||||
let uuid = UUID().uuidString.lowercased()
|
||||
let call = Call(direction: .outgoing, contact: contact, callUUID: uuid, callState: .waitCapabilities, localMedia: media)
|
||||
call.speakerEnabled = media == .video
|
||||
ChatModel.shared.activeCall = call
|
||||
return uuid
|
||||
}
|
||||
|
||||
func startOutgoingCall(callUUID: UUID) -> Bool {
|
||||
func startOutgoingCall(callUUID: String) -> Bool {
|
||||
let m = ChatModel.shared
|
||||
if let call = m.activeCall, call.callkitUUID == callUUID {
|
||||
if let call = m.activeCall, call.callUUID == callUUID {
|
||||
m.showCallView = true
|
||||
Task { await m.callCommand.processCommand(.capabilities(media: call.localMedia)) }
|
||||
return true
|
||||
@@ -28,7 +28,7 @@ class CallManager {
|
||||
return false
|
||||
}
|
||||
|
||||
func answerIncomingCall(callUUID: UUID) -> Bool {
|
||||
func answerIncomingCall(callUUID: String) -> Bool {
|
||||
if let invitation = getCallInvitation(callUUID) {
|
||||
answerIncomingCall(invitation: invitation)
|
||||
return true
|
||||
@@ -42,7 +42,7 @@ class CallManager {
|
||||
let call = Call(
|
||||
direction: .incoming,
|
||||
contact: invitation.contact,
|
||||
callkitUUID: invitation.callkitUUID,
|
||||
callUUID: invitation.callUUID,
|
||||
callState: .invitationAccepted,
|
||||
localMedia: invitation.callType.media,
|
||||
sharedKey: invitation.sharedKey
|
||||
@@ -68,8 +68,8 @@ class CallManager {
|
||||
}
|
||||
}
|
||||
|
||||
func enableMedia(media: CallMediaType, enable: Bool, callUUID: UUID) -> Bool {
|
||||
if let call = ChatModel.shared.activeCall, call.callkitUUID == callUUID {
|
||||
func enableMedia(media: CallMediaType, enable: Bool, callUUID: String) -> Bool {
|
||||
if let call = ChatModel.shared.activeCall, call.callUUID == callUUID {
|
||||
let m = ChatModel.shared
|
||||
Task { await m.callCommand.processCommand(.media(media: media, enable: enable)) }
|
||||
return true
|
||||
@@ -77,8 +77,8 @@ class CallManager {
|
||||
return false
|
||||
}
|
||||
|
||||
func endCall(callUUID: UUID, completed: @escaping (Bool) -> Void) {
|
||||
if let call = ChatModel.shared.activeCall, call.callkitUUID == callUUID {
|
||||
func endCall(callUUID: String, completed: @escaping (Bool) -> Void) {
|
||||
if let call = ChatModel.shared.activeCall, call.callUUID == callUUID {
|
||||
endCall(call: call) { completed(true) }
|
||||
} else if let invitation = getCallInvitation(callUUID) {
|
||||
endCall(invitation: invitation) { completed(true) }
|
||||
@@ -126,8 +126,8 @@ class CallManager {
|
||||
}
|
||||
}
|
||||
|
||||
private func getCallInvitation(_ callUUID: UUID) -> RcvCallInvitation? {
|
||||
if let (_, invitation) = ChatModel.shared.callInvitations.first(where: { (_, inv) in inv.callkitUUID == callUUID }) {
|
||||
private func getCallInvitation(_ callUUID: String) -> RcvCallInvitation? {
|
||||
if let (_, invitation) = ChatModel.shared.callInvitations.first(where: { (_, inv) in inv.callUUID == callUUID }) {
|
||||
return invitation
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -18,7 +18,7 @@ class Call: ObservableObject, Equatable {
|
||||
|
||||
var direction: CallDirection
|
||||
var contact: Contact
|
||||
var callkitUUID: UUID?
|
||||
var callUUID: String?
|
||||
var localMedia: CallMediaType
|
||||
@Published var callState: CallState
|
||||
@Published var localCapabilities: CallCapabilities?
|
||||
@@ -33,14 +33,14 @@ class Call: ObservableObject, Equatable {
|
||||
init(
|
||||
direction: CallDirection,
|
||||
contact: Contact,
|
||||
callkitUUID: UUID?,
|
||||
callUUID: String?,
|
||||
callState: CallState,
|
||||
localMedia: CallMediaType,
|
||||
sharedKey: String? = nil
|
||||
) {
|
||||
self.direction = direction
|
||||
self.contact = contact
|
||||
self.callkitUUID = callkitUUID
|
||||
self.callUUID = callUUID
|
||||
self.callState = callState
|
||||
self.localMedia = localMedia
|
||||
self.sharedKey = sharedKey
|
||||
|
||||
@@ -568,8 +568,8 @@ struct ChatView: View {
|
||||
|
||||
private func endCallButton(_ call: Call) -> some View {
|
||||
Button {
|
||||
if let uuid = call.callkitUUID {
|
||||
CallController.shared.endCall(callUUID: uuid)
|
||||
if CallController.useCallKit(), let callUUID = call.callUUID {
|
||||
CallController.shared.endCall(callUUID: callUUID)
|
||||
} else {
|
||||
CallController.shared.endCall(call: call) {}
|
||||
}
|
||||
|
||||
@@ -407,12 +407,18 @@ struct ServersSummaryView: View {
|
||||
|
||||
struct SubscriptionStatusIndicatorView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var subs: SMPServerSubs
|
||||
var hasSess: Bool
|
||||
|
||||
var body: some View {
|
||||
let onionHosts = networkUseOnionHostsGroupDefault.get()
|
||||
let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, hasSess)
|
||||
let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage(
|
||||
online: m.networkInfo.online,
|
||||
usesProxy: networkUseOnionHostsGroupDefault.get() != .no || groupDefaults.string(forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY) != nil,
|
||||
subs: subs,
|
||||
hasSess: hasSess,
|
||||
primaryColor: theme.colors.primary
|
||||
)
|
||||
if #available(iOS 16.0, *) {
|
||||
Image(systemName: "dot.radiowaves.up.forward", variableValue: variableValue)
|
||||
.foregroundColor(color)
|
||||
@@ -425,26 +431,32 @@ struct SubscriptionStatusIndicatorView: View {
|
||||
|
||||
struct SubscriptionStatusPercentageView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var subs: SMPServerSubs
|
||||
var hasSess: Bool
|
||||
|
||||
var body: some View {
|
||||
let onionHosts = networkUseOnionHostsGroupDefault.get()
|
||||
let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, hasSess)
|
||||
let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(
|
||||
online: m.networkInfo.online,
|
||||
usesProxy: networkUseOnionHostsGroupDefault.get() != .no || groupDefaults.string(forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY) != nil,
|
||||
subs: subs,
|
||||
hasSess: hasSess,
|
||||
primaryColor: theme.colors.primary
|
||||
)
|
||||
Text(verbatim: "\(Int(floor(statusPercent * 100)))%")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
func subscriptionStatusColorAndPercentage(_ online: Bool, _ onionHosts: OnionHosts, _ subs: SMPServerSubs, _ hasSess: Bool) -> (Color, Double, Double, Double) {
|
||||
func subscriptionStatusColorAndPercentage(online: Bool, usesProxy: Bool, subs: SMPServerSubs, hasSess: Bool, primaryColor: Color) -> (Color, Double, Double, Double) {
|
||||
func roundedToQuarter(_ n: Double) -> Double {
|
||||
n >= 1 ? 1
|
||||
: n <= 0 ? 0
|
||||
: (n * 4).rounded() / 4
|
||||
}
|
||||
|
||||
let activeColor: Color = onionHosts == .require ? .indigo : .accentColor
|
||||
let activeColor: Color = usesProxy ? .indigo : primaryColor
|
||||
let noConnColorAndPercent: (Color, Double, Double, Double) = (Color(uiColor: .tertiaryLabel), 1, 1, 0)
|
||||
let activeSubsRounded = roundedToQuarter(subs.shareOfActive)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -523,9 +534,15 @@ struct MigrateFromDevice: View {
|
||||
}
|
||||
case let .sndStandaloneFileComplete(_, fileTransferMeta, rcvURIs):
|
||||
let cfg = getNetCfg()
|
||||
let proxy: NetworkProxy? = if cfg.socksProxy == nil {
|
||||
nil
|
||||
} else {
|
||||
networkProxyDefault.get()
|
||||
}
|
||||
let data = MigrationFileLinkData.init(
|
||||
networkConfig: MigrationFileLinkData.NetworkConfig(
|
||||
socksProxy: cfg.socksProxy,
|
||||
networkProxy: proxy,
|
||||
hostMode: cfg.hostMode,
|
||||
requiredHostMode: cfg.requiredHostMode
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -36,6 +36,10 @@ struct AdvancedNetworkSettings: View {
|
||||
@State private var showSettingsAlert: NetworkSettingsAlert?
|
||||
@State private var onionHosts: OnionHosts = .no
|
||||
@State private var showSaveDialog = false
|
||||
@State private var netProxy = networkProxyDefault.get()
|
||||
@State private var currentNetProxy = networkProxyDefault.get()
|
||||
@State private var useNetProxy = false
|
||||
@State private var netProxyAuth = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
@@ -102,6 +106,76 @@ struct AdvancedNetworkSettings: View {
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle("Use SOCKS proxy", isOn: $useNetProxy)
|
||||
Group {
|
||||
TextField("IP address", text: $netProxy.host)
|
||||
TextField(
|
||||
"Port",
|
||||
text: Binding(
|
||||
get: { netProxy.port > 0 ? "\(netProxy.port)" : "" },
|
||||
set: { s in
|
||||
netProxy.port = if let port = Int(s), port > 0 {
|
||||
port
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
Toggle("Proxy requires password", isOn: $netProxyAuth)
|
||||
if netProxyAuth {
|
||||
TextField("Username", text: $netProxy.username)
|
||||
PassphraseField(
|
||||
key: $netProxy.password,
|
||||
placeholder: "Password",
|
||||
valid: NetworkProxy.validCredential(netProxy.password)
|
||||
)
|
||||
}
|
||||
}
|
||||
.if(!useNetProxy) { $0.foregroundColor(theme.colors.secondary) }
|
||||
.disabled(!useNetProxy)
|
||||
} header: {
|
||||
HStack {
|
||||
Text("SOCKS proxy").foregroundColor(theme.colors.secondary)
|
||||
if useNetProxy && !netProxy.valid {
|
||||
Spacer()
|
||||
Image(systemName: "exclamationmark.circle.fill").foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
} footer: {
|
||||
if netProxyAuth {
|
||||
Text("Your credentials may be sent unencrypted.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} else {
|
||||
Text("Do not use credentials with proxy.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
.onChange(of: useNetProxy) { useNetProxy in
|
||||
netCfg.socksProxy = useNetProxy && currentNetProxy.valid
|
||||
? currentNetProxy.toProxyString()
|
||||
: nil
|
||||
netProxy = currentNetProxy
|
||||
netProxyAuth = netProxy.username != "" || netProxy.password != ""
|
||||
}
|
||||
.onChange(of: netProxyAuth) { netProxyAuth in
|
||||
if netProxyAuth {
|
||||
netProxy.auth = currentNetProxy.auth
|
||||
netProxy.username = currentNetProxy.username
|
||||
netProxy.password = currentNetProxy.password
|
||||
} else {
|
||||
netProxy.auth = .username
|
||||
netProxy.username = ""
|
||||
netProxy.password = ""
|
||||
}
|
||||
}
|
||||
.onChange(of: netProxy) { netProxy in
|
||||
netCfg.socksProxy = useNetProxy && netProxy.valid
|
||||
? netProxy.toProxyString()
|
||||
: nil
|
||||
}
|
||||
|
||||
Section {
|
||||
Picker("Use .onion hosts", selection: $onionHosts) {
|
||||
ForEach(OnionHosts.values, id: \.self) { Text($0.text) }
|
||||
@@ -156,19 +230,19 @@ struct AdvancedNetworkSettings: View {
|
||||
|
||||
Section {
|
||||
Button("Reset to defaults") {
|
||||
updateNetCfgView(NetCfg.defaults)
|
||||
updateNetCfgView(NetCfg.defaults, NetworkProxy.def)
|
||||
}
|
||||
.disabled(netCfg == NetCfg.defaults)
|
||||
|
||||
Button("Set timeouts for proxy/VPN") {
|
||||
updateNetCfgView(netCfg.withProxyTimeouts)
|
||||
updateNetCfgView(netCfg.withProxyTimeouts, netProxy)
|
||||
}
|
||||
.disabled(netCfg.hasProxyTimeouts)
|
||||
|
||||
Button("Save and reconnect") {
|
||||
showSettingsAlert = .update
|
||||
}
|
||||
.disabled(netCfg == currentNetCfg)
|
||||
.disabled(netCfg == currentNetCfg || (useNetProxy && !netProxy.valid))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,7 +256,8 @@ struct AdvancedNetworkSettings: View {
|
||||
if cfgLoaded { return }
|
||||
cfgLoaded = true
|
||||
currentNetCfg = getNetCfg()
|
||||
updateNetCfgView(currentNetCfg)
|
||||
currentNetProxy = networkProxyDefault.get()
|
||||
updateNetCfgView(currentNetCfg, currentNetProxy)
|
||||
}
|
||||
.alert(item: $showSettingsAlert) { a in
|
||||
switch a {
|
||||
@@ -206,7 +281,7 @@ struct AdvancedNetworkSettings: View {
|
||||
if netCfg == currentNetCfg {
|
||||
dismiss()
|
||||
cfgLoaded = false
|
||||
} else {
|
||||
} else if !useNetProxy || netProxy.valid {
|
||||
showSaveDialog = true
|
||||
}
|
||||
})
|
||||
@@ -221,18 +296,26 @@ struct AdvancedNetworkSettings: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func updateNetCfgView(_ cfg: NetCfg) {
|
||||
private func updateNetCfgView(_ cfg: NetCfg, _ proxy: NetworkProxy) {
|
||||
netCfg = cfg
|
||||
netProxy = proxy
|
||||
onionHosts = OnionHosts(netCfg: netCfg)
|
||||
enableKeepAlive = netCfg.enableKeepAlive
|
||||
keepAliveOpts = netCfg.tcpKeepAlive ?? KeepAliveOpts.defaults
|
||||
useNetProxy = netCfg.socksProxy != nil
|
||||
netProxyAuth = switch netProxy.auth {
|
||||
case .username: netProxy.username != "" || netProxy.password != ""
|
||||
case .isolate: false
|
||||
}
|
||||
}
|
||||
|
||||
private func saveNetCfg() -> Bool {
|
||||
do {
|
||||
try setNetworkConfig(netCfg)
|
||||
currentNetCfg = netCfg
|
||||
setNetCfg(netCfg)
|
||||
setNetCfg(netCfg, networkProxy: useNetProxy ? netProxy : nil)
|
||||
currentNetProxy = netProxy
|
||||
networkProxyDefault.set(netProxy)
|
||||
return true
|
||||
} catch let error {
|
||||
let err = responseError(error)
|
||||
|
||||
@@ -19,9 +19,15 @@ extension AppSettings {
|
||||
val.hostMode = .publicHost
|
||||
val.requiredHostMode = true
|
||||
}
|
||||
val.socksProxy = nil
|
||||
setNetCfg(val)
|
||||
if val.socksProxy != nil {
|
||||
val.socksProxy = networkProxy?.toProxyString()
|
||||
setNetCfg(val, networkProxy: networkProxy)
|
||||
} else {
|
||||
val.socksProxy = nil
|
||||
setNetCfg(val, networkProxy: nil)
|
||||
}
|
||||
}
|
||||
if let val = networkProxy { networkProxyDefault.set(val) }
|
||||
if let val = privacyEncryptLocalFiles { privacyEncryptLocalFilesGroupDefault.set(val) }
|
||||
if let val = privacyAskToApproveRelays { privacyAskToApproveRelaysGroupDefault.set(val) }
|
||||
if let val = privacyAcceptImages {
|
||||
@@ -52,10 +58,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) }
|
||||
}
|
||||
|
||||
@@ -63,6 +69,7 @@ extension AppSettings {
|
||||
let def = UserDefaults.standard
|
||||
var c = AppSettings.defaults
|
||||
c.networkConfig = getNetCfg()
|
||||
c.networkProxy = networkProxyDefault.get()
|
||||
c.privacyEncryptLocalFiles = privacyEncryptLocalFilesGroupDefault.get()
|
||||
c.privacyAskToApproveRelays = privacyAskToApproveRelaysGroupDefault.get()
|
||||
c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get()
|
||||
|
||||
@@ -73,6 +73,8 @@ let DEFAULT_SYSTEM_DARK_THEME = "systemDarkTheme"
|
||||
let DEFAULT_CURRENT_THEME_IDS = "currentThemeIds"
|
||||
let DEFAULT_THEME_OVERRIDES = "themeOverrides"
|
||||
|
||||
let DEFAULT_NETWORK_PROXY = "networkProxy"
|
||||
|
||||
let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen"
|
||||
|
||||
let appDefaults: [String: Any] = [
|
||||
@@ -245,6 +247,7 @@ public class CodableDefault<T: Codable> {
|
||||
}
|
||||
}
|
||||
|
||||
let networkProxyDefault: CodableDefault<NetworkProxy> = CodableDefault(defaults: UserDefaults.standard, forKey: DEFAULT_NETWORK_PROXY, withDefault: NetworkProxy.def)
|
||||
|
||||
struct SettingsView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@@ -339,7 +339,9 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
CXProvider.reportNewIncomingVoIPPushPayload([
|
||||
"displayName": invitation.contact.displayName,
|
||||
"contactId": invitation.contact.id,
|
||||
"media": invitation.callType.media.rawValue
|
||||
"callUUID": invitation.callUUID ?? "",
|
||||
"media": invitation.callType.media.rawValue,
|
||||
"callTs": invitation.callTs.timeIntervalSince1970
|
||||
]) { error in
|
||||
logger.debug("reportNewIncomingVoIPPushPayload result: \(error)")
|
||||
deliver(error == nil ? nil : createCallInvitationNtf(invitation))
|
||||
|
||||
@@ -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 */; };
|
||||
@@ -214,11 +216,11 @@
|
||||
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
|
||||
D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; };
|
||||
E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; };
|
||||
E51ED58A2C7A26FE009F2C7C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5852C7A26FE009F2C7C /* libffi.a */; };
|
||||
E51ED58B2C7A26FE009F2C7C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5862C7A26FE009F2C7C /* libgmpxx.a */; };
|
||||
E51ED58C2C7A26FE009F2C7C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5872C7A26FE009F2C7C /* libgmp.a */; };
|
||||
E51ED58D2C7A26FE009F2C7C /* libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5882C7A26FE009F2C7C /* libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4.a */; };
|
||||
E51ED58E2C7A26FE009F2C7C /* libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5892C7A26FE009F2C7C /* libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4-ghc9.6.3.a */; };
|
||||
E5CC47842CA31C3A00551ACF /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5CC477F2CA31C3900551ACF /* libgmpxx.a */; };
|
||||
E5CC47852CA31C3A00551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5CC47802CA31C3900551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP.a */; };
|
||||
E5CC47862CA31C3A00551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5CC47812CA31C3900551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP-ghc9.6.3.a */; };
|
||||
E5CC47872CA31C3A00551ACF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5CC47822CA31C3A00551ACF /* libgmp.a */; };
|
||||
E5CC47882CA31C3A00551ACF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5CC47832CA31C3A00551ACF /* libffi.a */; };
|
||||
E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
|
||||
E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; };
|
||||
E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9822C5902CE007928CC /* Localizable.strings */; };
|
||||
@@ -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>"; };
|
||||
@@ -550,11 +554,11 @@
|
||||
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||
D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
E51CC1E52C62085600DB91FE /* OneHandUICard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneHandUICard.swift; sourceTree = "<group>"; };
|
||||
E51ED5852C7A26FE009F2C7C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
E51ED5862C7A26FE009F2C7C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
E51ED5872C7A26FE009F2C7C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
E51ED5882C7A26FE009F2C7C /* libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4.a"; sourceTree = "<group>"; };
|
||||
E51ED5892C7A26FE009F2C7C /* libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
E5CC477F2CA31C3900551ACF /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
E5CC47802CA31C3900551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP.a"; sourceTree = "<group>"; };
|
||||
E5CC47812CA31C3900551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
E5CC47822CA31C3A00551ACF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
E5CC47832CA31C3A00551ACF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
E5DCF9732C590275007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
@@ -645,14 +649,14 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E51ED58D2C7A26FE009F2C7C /* libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4.a in Frameworks */,
|
||||
E51ED58E2C7A26FE009F2C7C /* libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4-ghc9.6.3.a in Frameworks */,
|
||||
E5CC47852CA31C3A00551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP.a in Frameworks */,
|
||||
E5CC47872CA31C3A00551ACF /* libgmp.a in Frameworks */,
|
||||
E5CC47842CA31C3A00551ACF /* libgmpxx.a in Frameworks */,
|
||||
E5CC47882CA31C3A00551ACF /* libffi.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
E51ED58B2C7A26FE009F2C7C /* libgmpxx.a in Frameworks */,
|
||||
E5CC47862CA31C3A00551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP-ghc9.6.3.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
|
||||
E51ED58C2C7A26FE009F2C7C /* libgmp.a in Frameworks */,
|
||||
E51ED58A2C7A26FE009F2C7C /* libffi.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -729,11 +733,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E51ED5852C7A26FE009F2C7C /* libffi.a */,
|
||||
E51ED5872C7A26FE009F2C7C /* libgmp.a */,
|
||||
E51ED5862C7A26FE009F2C7C /* libgmpxx.a */,
|
||||
E51ED5892C7A26FE009F2C7C /* libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4-ghc9.6.3.a */,
|
||||
E51ED5882C7A26FE009F2C7C /* libHSsimplex-chat-6.0.3.0-JVz5IxfwvrHaD2mJGTgT4.a */,
|
||||
E5CC47832CA31C3A00551ACF /* libffi.a */,
|
||||
E5CC47822CA31C3A00551ACF /* libgmp.a */,
|
||||
E5CC477F2CA31C3900551ACF /* libgmpxx.a */,
|
||||
E5CC47812CA31C3900551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP-ghc9.6.3.a */,
|
||||
E5CC47802CA31C3900551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
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 */,
|
||||
@@ -1879,7 +1887,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 235;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1904,7 +1912,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES_THIN;
|
||||
MARKETING_VERSION = 6.0.3;
|
||||
MARKETING_VERSION = 6.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1928,7 +1936,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 235;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1953,7 +1961,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.0.3;
|
||||
MARKETING_VERSION = 6.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1969,11 +1977,11 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 235;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 6.0.3;
|
||||
MARKETING_VERSION = 6.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1989,11 +1997,11 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 235;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 6.0.3;
|
||||
MARKETING_VERSION = 6.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2014,7 +2022,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 235;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
@@ -2029,7 +2037,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.0.3;
|
||||
MARKETING_VERSION = 6.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -2051,7 +2059,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 235;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
@@ -2066,7 +2074,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.0.3;
|
||||
MARKETING_VERSION = 6.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -2088,7 +2096,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 235;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2114,7 +2122,7 @@
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.0.3;
|
||||
MARKETING_VERSION = 6.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2139,7 +2147,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 235;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2165,7 +2173,7 @@
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.0.3;
|
||||
MARKETING_VERSION = 6.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2190,7 +2198,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 235;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -2205,7 +2213,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 6.0.3;
|
||||
MARKETING_VERSION = 6.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2224,7 +2232,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 235;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -2239,7 +2247,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 6.0.3;
|
||||
MARKETING_VERSION = 6.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Network
|
||||
|
||||
public let jsonDecoder = getJSONDecoder()
|
||||
public let jsonEncoder = getJSONEncoder()
|
||||
@@ -1476,6 +1477,63 @@ public struct KeepAliveOpts: Codable, Equatable {
|
||||
public static let defaults: KeepAliveOpts = KeepAliveOpts(keepIdle: 30, keepIntvl: 15, keepCnt: 4)
|
||||
}
|
||||
|
||||
public struct NetworkProxy: Equatable, Codable {
|
||||
public var host: String = ""
|
||||
public var port: Int = 0
|
||||
public var auth: NetworkProxyAuth = .username
|
||||
public var username: String = ""
|
||||
public var password: String = ""
|
||||
|
||||
public static var def: NetworkProxy {
|
||||
NetworkProxy()
|
||||
}
|
||||
|
||||
public var valid: Bool {
|
||||
let hostOk = switch NWEndpoint.Host(host) {
|
||||
case .ipv4: true
|
||||
case .ipv6: true
|
||||
default: false
|
||||
}
|
||||
return hostOk &&
|
||||
port > 0 && port <= 65535 &&
|
||||
NetworkProxy.validCredential(username) && NetworkProxy.validCredential(password)
|
||||
}
|
||||
|
||||
public static func validCredential(_ s: String) -> Bool {
|
||||
!s.contains(":") && !s.contains("@")
|
||||
}
|
||||
|
||||
public func toProxyString() -> String? {
|
||||
if !valid { return nil }
|
||||
var res = ""
|
||||
switch auth {
|
||||
case .username:
|
||||
let usernameTrimmed = username.trimmingCharacters(in: .whitespaces)
|
||||
let passwordTrimmed = password.trimmingCharacters(in: .whitespaces)
|
||||
if usernameTrimmed != "" || passwordTrimmed != "" {
|
||||
res += usernameTrimmed + ":" + passwordTrimmed + "@"
|
||||
} else {
|
||||
res += "@"
|
||||
}
|
||||
case .isolate: ()
|
||||
}
|
||||
if host != "" {
|
||||
if host.contains(":") {
|
||||
res += "[\(host.trimmingCharacters(in: [" ", "[", "]"]))]"
|
||||
} else {
|
||||
res += host.trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
}
|
||||
res += ":\(port)"
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
public enum NetworkProxyAuth: String, Codable {
|
||||
case username
|
||||
case isolate
|
||||
}
|
||||
|
||||
public enum NetworkStatus: Decodable, Equatable {
|
||||
case unknown
|
||||
case connected
|
||||
@@ -2099,11 +2157,13 @@ public struct MigrationFileLinkData: Codable {
|
||||
|
||||
public struct NetworkConfig: Codable {
|
||||
let socksProxy: String?
|
||||
let networkProxy: NetworkProxy?
|
||||
let hostMode: HostMode?
|
||||
let requiredHostMode: Bool?
|
||||
|
||||
public init(socksProxy: String?, hostMode: HostMode?, requiredHostMode: Bool?) {
|
||||
public init(socksProxy: String?, networkProxy: NetworkProxy?, hostMode: HostMode?, requiredHostMode: Bool?) {
|
||||
self.socksProxy = socksProxy
|
||||
self.networkProxy = networkProxy
|
||||
self.hostMode = hostMode
|
||||
self.requiredHostMode = requiredHostMode
|
||||
}
|
||||
@@ -2112,6 +2172,7 @@ public struct MigrationFileLinkData: Codable {
|
||||
return if let hostMode, let requiredHostMode {
|
||||
NetworkConfig(
|
||||
socksProxy: nil,
|
||||
networkProxy: nil,
|
||||
hostMode: hostMode == .onionViaSocks ? .onionHost : hostMode,
|
||||
requiredHostMode: requiredHostMode
|
||||
)
|
||||
@@ -2131,6 +2192,7 @@ public struct MigrationFileLinkData: Codable {
|
||||
|
||||
public struct AppSettings: Codable, Equatable {
|
||||
public var networkConfig: NetCfg? = nil
|
||||
public var networkProxy: NetworkProxy? = nil
|
||||
public var privacyEncryptLocalFiles: Bool? = nil
|
||||
public var privacyAskToApproveRelays: Bool? = nil
|
||||
public var privacyAcceptImages: Bool? = nil
|
||||
@@ -2162,6 +2224,7 @@ public struct AppSettings: Codable, Equatable {
|
||||
var empty = AppSettings()
|
||||
let def = AppSettings.defaults
|
||||
if networkConfig != def.networkConfig { empty.networkConfig = networkConfig }
|
||||
if networkProxy != def.networkProxy { empty.networkProxy = networkProxy }
|
||||
if privacyEncryptLocalFiles != def.privacyEncryptLocalFiles { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles }
|
||||
if privacyAskToApproveRelays != def.privacyAskToApproveRelays { empty.privacyAskToApproveRelays = privacyAskToApproveRelays }
|
||||
if privacyAcceptImages != def.privacyAcceptImages { empty.privacyAcceptImages = privacyAcceptImages }
|
||||
@@ -2194,6 +2257,7 @@ public struct AppSettings: Codable, Equatable {
|
||||
public static var defaults: AppSettings {
|
||||
AppSettings (
|
||||
networkConfig: NetCfg.defaults,
|
||||
networkProxy: NetworkProxy.def,
|
||||
privacyEncryptLocalFiles: true,
|
||||
privacyAskToApproveRelays: true,
|
||||
privacyAcceptImages: true,
|
||||
|
||||
@@ -35,6 +35,7 @@ public let GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS = "privacyAskToApproveRel
|
||||
// replaces DEFAULT_PROFILE_IMAGE_CORNER_RADIUS
|
||||
public let GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius"
|
||||
let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount"
|
||||
public let GROUP_DEFAULT_NETWORK_SOCKS_PROXY = "networkSocksProxy"
|
||||
let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts"
|
||||
let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode"
|
||||
let GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE = "networkSMPProxyMode"
|
||||
@@ -327,6 +328,7 @@ public class Default<T> {
|
||||
}
|
||||
|
||||
public func getNetCfg() -> NetCfg {
|
||||
let socksProxy = groupDefaults.string(forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY)
|
||||
let onionHosts = networkUseOnionHostsGroupDefault.get()
|
||||
let (hostMode, requiredHostMode) = onionHosts.hostMode
|
||||
let sessionMode = networkSessionModeGroupDefault.get()
|
||||
@@ -349,6 +351,7 @@ public func getNetCfg() -> NetCfg {
|
||||
tcpKeepAlive = nil
|
||||
}
|
||||
return NetCfg(
|
||||
socksProxy: socksProxy,
|
||||
hostMode: hostMode,
|
||||
requiredHostMode: requiredHostMode,
|
||||
sessionMode: sessionMode,
|
||||
@@ -365,11 +368,13 @@ public func getNetCfg() -> NetCfg {
|
||||
)
|
||||
}
|
||||
|
||||
public func setNetCfg(_ cfg: NetCfg) {
|
||||
public func setNetCfg(_ cfg: NetCfg, networkProxy: NetworkProxy?) {
|
||||
networkUseOnionHostsGroupDefault.set(OnionHosts(netCfg: cfg))
|
||||
networkSessionModeGroupDefault.set(cfg.sessionMode)
|
||||
networkSMPProxyModeGroupDefault.set(cfg.smpProxyMode)
|
||||
networkSMPProxyFallbackGroupDefault.set(cfg.smpProxyFallback)
|
||||
let socksProxy = networkProxy?.toProxyString()
|
||||
groupDefaults.set(socksProxy, forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY)
|
||||
groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT)
|
||||
groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT)
|
||||
groupDefaults.set(cfg.tcpTimeoutPerKb, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB)
|
||||
|
||||
@@ -42,6 +42,7 @@ public struct RcvCallInvitation: Decodable {
|
||||
public var contact: Contact
|
||||
public var callType: CallType
|
||||
public var sharedKey: String?
|
||||
public var callUUID: String?
|
||||
public var callTs: Date
|
||||
public var callTypeText: LocalizedStringKey {
|
||||
get {
|
||||
@@ -52,10 +53,8 @@ public struct RcvCallInvitation: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public var callkitUUID: UUID? = UUID()
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case user, contact, callType, sharedKey, callTs
|
||||
case user, contact, callType, sharedKey, callUUID, callTs
|
||||
}
|
||||
|
||||
public static let sampleData = RcvCallInvitation(
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#define SimpleX_h
|
||||
|
||||
#include "hs_init.h"
|
||||
#include "objc.h"
|
||||
|
||||
extern void hs_init(int argc, char **argv[]);
|
||||
|
||||
|
||||
@@ -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 */
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
+6
-5
@@ -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")
|
||||
@@ -424,6 +424,7 @@ fun PreviewIncomingCallLockScreenAlert() {
|
||||
) {
|
||||
IncomingCallLockScreenAlertLayout(
|
||||
invitation = RcvCallInvitation(
|
||||
callUUID = "",
|
||||
remoteHostId = null,
|
||||
user = User.sampleData,
|
||||
contact = Contact.sampleData,
|
||||
|
||||
@@ -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>
|
||||
@@ -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,31 +78,36 @@ 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 {
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.8.0")
|
||||
implementation("com.github.Dansoftowner:jSystemThemeDetector:3.8")
|
||||
implementation("com.github.Dansoftowner:jSystemThemeDetector:3.8") {
|
||||
exclude("net.java.dev.jna")
|
||||
}
|
||||
// For jSystemThemeDetector only
|
||||
implementation("net.java.dev.jna:jna-platform:5.14.0")
|
||||
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("net.java.dev.jna:jna:5.14.0")
|
||||
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 +124,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")
|
||||
|
||||
+15
@@ -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
@@ -770,6 +770,7 @@ fun PreviewActiveCallOverlayVideo() {
|
||||
callState = CallState.Negotiated,
|
||||
localMedia = CallMediaType.Video,
|
||||
peerMedia = CallMediaType.Video,
|
||||
callUUID = "",
|
||||
connectionInfo = ConnectionInfo(
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "tcp"),
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "tcp")
|
||||
@@ -799,6 +800,7 @@ fun PreviewActiveCallOverlayAudio() {
|
||||
callState = CallState.Negotiated,
|
||||
localMedia = CallMediaType.Audio,
|
||||
peerMedia = CallMediaType.Audio,
|
||||
callUUID = "",
|
||||
connectionInfo = ConnectionInfo(
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "udp"),
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "udp")
|
||||
|
||||
+2
-1
@@ -14,6 +14,7 @@ actual fun authenticate(
|
||||
promptSubtitle: String,
|
||||
selfDestruct: Boolean,
|
||||
usingLAMode: LAMode,
|
||||
oneTime: Boolean,
|
||||
completed: (LAResult) -> Unit
|
||||
) {
|
||||
val activity = mainActivity.get() ?: return completed(LAResult.Error(""))
|
||||
@@ -27,7 +28,7 @@ actual fun authenticate(
|
||||
else -> completed(LAResult.Unavailable())
|
||||
}
|
||||
LAMode.PASSCODE -> {
|
||||
authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, completed)
|
||||
authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, oneTime, completed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -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>
|
||||
|
||||
@@ -74,6 +74,7 @@ fun MainScreen() {
|
||||
LaunchedEffect(showAdvertiseLAAlert) {
|
||||
if (
|
||||
!chatModel.controller.appPrefs.laNoticeShown.get()
|
||||
&& !appPrefs.performLA.get()
|
||||
&& showAdvertiseLAAlert
|
||||
&& chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete
|
||||
&& chatModel.chats.size > 3
|
||||
@@ -211,10 +212,8 @@ fun MainScreen() {
|
||||
} else {
|
||||
ActiveCallView()
|
||||
}
|
||||
} else {
|
||||
// It's needed for privacy settings toggle, so it can be shown even if the app is passcode unlocked
|
||||
ModalManager.fullscreen.showPasscodeInView()
|
||||
}
|
||||
ModalManager.fullscreen.showOneTimePasscodeInView()
|
||||
AlertManager.privacySensitive.showInView()
|
||||
if (onboarding == OnboardingStage.OnboardingComplete) {
|
||||
LaunchedEffect(chatModel.currentUser.value, chatModel.appOpenUrl.value) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.localauth.SetAppPasscodeView
|
||||
@@ -31,7 +32,7 @@ object AppLock {
|
||||
|
||||
fun showLANotice(laNoticeShown: SharedPreference<Boolean>) {
|
||||
Log.d(TAG, "showLANotice")
|
||||
if (!laNoticeShown.get()) {
|
||||
if (!laNoticeShown.get() && !appPrefs.performLA.get()) {
|
||||
laNoticeShown.set(true)
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.la_notice_title_simplex_lock),
|
||||
@@ -57,6 +58,8 @@ object AppLock {
|
||||
|
||||
private fun showChooseLAMode() {
|
||||
Log.d(TAG, "showLANotice")
|
||||
if (appPrefs.performLA.get()) return
|
||||
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(MR.strings.la_lock_mode),
|
||||
text = null,
|
||||
@@ -80,21 +83,23 @@ object AppLock {
|
||||
authenticate(
|
||||
generalGetString(MR.strings.auth_enable_simplex_lock),
|
||||
generalGetString(MR.strings.auth_confirm_credential),
|
||||
oneTime = true,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
m.performLA.value = true
|
||||
m.showAuthScreen.value = true
|
||||
appPrefs.performLA.set(true)
|
||||
laTurnedOnAlert()
|
||||
}
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
is LAResult.Error -> {
|
||||
m.performLA.value = false
|
||||
appPrefs.performLA.set(false)
|
||||
m.showAuthScreen.value = false
|
||||
// Don't drop auth pref in case of state inconsistency (eg, you have set passcode but somehow bypassed toggle and turned it off and then on)
|
||||
// appPrefs.performLA.set(false)
|
||||
laFailedAlert()
|
||||
}
|
||||
is LAResult.Unavailable -> {
|
||||
m.performLA.value = false
|
||||
m.showAuthScreen.value = false
|
||||
appPrefs.performLA.set(false)
|
||||
m.showAdvertiseLAUnavailableAlert.value = true
|
||||
}
|
||||
@@ -104,19 +109,22 @@ object AppLock {
|
||||
}
|
||||
|
||||
private fun setPasscode() {
|
||||
if (appPrefs.performLA.get()) return
|
||||
|
||||
val appPrefs = ChatController.appPrefs
|
||||
ModalManager.fullscreen.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||
SetAppPasscodeView(
|
||||
submit = {
|
||||
ChatModel.performLA.value = true
|
||||
ChatModel.showAuthScreen.value = true
|
||||
appPrefs.performLA.set(true)
|
||||
appPrefs.laMode.set(LAMode.PASSCODE)
|
||||
laTurnedOnAlert()
|
||||
},
|
||||
cancel = {
|
||||
ChatModel.performLA.value = false
|
||||
appPrefs.performLA.set(false)
|
||||
ChatModel.showAuthScreen.value = false
|
||||
// Don't drop auth pref in case of state inconsistency (eg, you have set passcode but somehow bypassed toggle and turned it off and then on)
|
||||
// appPrefs.performLA.set(false)
|
||||
laPasscodeNotSetAlert()
|
||||
},
|
||||
close = close
|
||||
@@ -147,6 +155,7 @@ object AppLock {
|
||||
else
|
||||
generalGetString(MR.strings.auth_unlock),
|
||||
selfDestruct = true,
|
||||
oneTime = false,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success ->
|
||||
@@ -160,7 +169,7 @@ object AppLock {
|
||||
}
|
||||
is LAResult.Unavailable -> {
|
||||
userAuthorized.value = true
|
||||
m.performLA.value = false
|
||||
m.showAuthScreen.value = false
|
||||
m.controller.appPrefs.performLA.set(false)
|
||||
laUnavailableTurningOffAlert()
|
||||
}
|
||||
@@ -192,22 +201,23 @@ object AppLock {
|
||||
generalGetString(MR.strings.auth_confirm_credential)
|
||||
else
|
||||
"",
|
||||
oneTime = true,
|
||||
completed = { laResult ->
|
||||
val prefPerformLA = m.controller.appPrefs.performLA
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
m.performLA.value = true
|
||||
m.showAuthScreen.value = true
|
||||
prefPerformLA.set(true)
|
||||
laTurnedOnAlert()
|
||||
}
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
is LAResult.Error -> {
|
||||
m.performLA.value = false
|
||||
m.showAuthScreen.value = false
|
||||
prefPerformLA.set(false)
|
||||
laFailedAlert()
|
||||
}
|
||||
is LAResult.Unavailable -> {
|
||||
m.performLA.value = false
|
||||
m.showAuthScreen.value = false
|
||||
prefPerformLA.set(false)
|
||||
laUnavailableInstructionAlert()
|
||||
}
|
||||
@@ -227,12 +237,13 @@ object AppLock {
|
||||
generalGetString(MR.strings.auth_confirm_credential)
|
||||
else
|
||||
generalGetString(MR.strings.auth_disable_simplex_lock),
|
||||
oneTime = true,
|
||||
completed = { laResult ->
|
||||
val prefPerformLA = m.controller.appPrefs.performLA
|
||||
val selfDestructPref = m.controller.appPrefs.selfDestruct
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
m.performLA.value = false
|
||||
m.showAuthScreen.value = false
|
||||
prefPerformLA.set(false)
|
||||
DatabaseUtils.ksAppPassword.remove()
|
||||
selfDestructPref.set(false)
|
||||
@@ -240,12 +251,12 @@ object AppLock {
|
||||
}
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
is LAResult.Error -> {
|
||||
m.performLA.value = true
|
||||
m.showAuthScreen.value = true
|
||||
prefPerformLA.set(true)
|
||||
laFailedAlert()
|
||||
}
|
||||
is LAResult.Unavailable -> {
|
||||
m.performLA.value = false
|
||||
m.showAuthScreen.value = false
|
||||
prefPerformLA.set(false)
|
||||
laUnavailableTurningOffAlert()
|
||||
}
|
||||
|
||||
+4
-3
@@ -20,7 +20,6 @@ import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.internal.ChannelFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.datetime.*
|
||||
@@ -98,7 +97,7 @@ object ChatModel {
|
||||
}
|
||||
)
|
||||
}
|
||||
val performLA by lazy { mutableStateOf(ChatController.appPrefs.performLA.get()) }
|
||||
val showAuthScreen by lazy { mutableStateOf(ChatController.appPrefs.performLA.get()) }
|
||||
val showAdvertiseLAUnavailableAlert = mutableStateOf(false)
|
||||
val showChatPreviews by lazy { mutableStateOf(ChatController.appPrefs.privacyShowChatPreviews.get()) }
|
||||
|
||||
@@ -412,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
|
||||
}
|
||||
|
||||
+71
-21
@@ -129,7 +129,22 @@ class AppPreferences {
|
||||
val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false)
|
||||
val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false)
|
||||
val networkShowSubscriptionPercentage = mkBoolPreference(SHARED_PREFS_NETWORK_SHOW_SUBSCRIPTION_PERCENTAGE, false)
|
||||
val networkProxyHostPort = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, "localhost:9050")
|
||||
private val _networkProxy = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, json.encodeToString(NetworkProxy()))
|
||||
val networkProxy: SharedPreference<NetworkProxy> = SharedPreference(
|
||||
get = fun(): NetworkProxy {
|
||||
val value = _networkProxy.get() ?: return NetworkProxy()
|
||||
return try {
|
||||
if (value.startsWith("{")) {
|
||||
json.decodeFromString(value)
|
||||
} else {
|
||||
NetworkProxy(host = value.substringBefore(":").ifBlank { "localhost" }, port = value.substringAfter(":").toIntOrNull() ?: 9050)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
NetworkProxy()
|
||||
}
|
||||
},
|
||||
set = fun(proxy: NetworkProxy) { _networkProxy.set(json.encodeToString(proxy)) }
|
||||
)
|
||||
private val _networkSessionMode = mkStrPreference(SHARED_PREFS_NETWORK_SESSION_MODE, TransportSessionMode.default.name)
|
||||
val networkSessionMode: SharedPreference<TransportSessionMode> = SharedPreference(
|
||||
get = fun(): TransportSessionMode {
|
||||
@@ -531,7 +546,7 @@ object ChatController {
|
||||
suspend fun startChatWithTemporaryDatabase(ctrl: ChatCtrl, netCfg: NetCfg): User? {
|
||||
Log.d(TAG, "startChatWithTemporaryDatabase")
|
||||
val migrationActiveUser = apiGetActiveUser(null, ctrl) ?: apiCreateActiveUser(null, Profile(displayName = "Temp", fullName = ""), ctrl = ctrl)
|
||||
if (!apiSetNetworkConfig(netCfg, ctrl)) {
|
||||
if (!apiSetNetworkConfig(netCfg, ctrl = ctrl)) {
|
||||
Log.e(TAG, "Error setting network config, stopping migration")
|
||||
return null
|
||||
}
|
||||
@@ -976,16 +991,18 @@ object ChatController {
|
||||
throw Exception("failed to set chat item TTL: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiSetNetworkConfig(cfg: NetCfg, ctrl: ChatCtrl? = null): Boolean {
|
||||
suspend fun apiSetNetworkConfig(cfg: NetCfg, showAlertOnError: Boolean = true, ctrl: ChatCtrl? = null): Boolean {
|
||||
val r = sendCmd(null, CC.APISetNetworkConfig(cfg), ctrl)
|
||||
return when (r) {
|
||||
is CR.CmdOk -> true
|
||||
else -> {
|
||||
Log.e(TAG, "apiSetNetworkConfig bad response: ${r.responseType} ${r.details}")
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.error_setting_network_config),
|
||||
"${r.responseType}: ${r.details}"
|
||||
)
|
||||
if (showAlertOnError) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.error_setting_network_config),
|
||||
"${r.responseType}: ${r.details}"
|
||||
)
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -2745,13 +2762,9 @@ object ChatController {
|
||||
|
||||
fun getNetCfg(): NetCfg {
|
||||
val useSocksProxy = appPrefs.networkUseSocksProxy.get()
|
||||
val proxyHostPort = appPrefs.networkProxyHostPort.get()
|
||||
val networkProxy = appPrefs.networkProxy.get()
|
||||
val socksProxy = if (useSocksProxy) {
|
||||
if (proxyHostPort?.startsWith("localhost:") == true) {
|
||||
proxyHostPort.removePrefix("localhost")
|
||||
} else {
|
||||
proxyHostPort ?: ":9050"
|
||||
}
|
||||
networkProxy.toProxyString()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -2793,7 +2806,7 @@ object ChatController {
|
||||
}
|
||||
|
||||
/**
|
||||
* [AppPreferences.networkProxyHostPort] is not changed here, use appPrefs to set it
|
||||
* [AppPreferences.networkProxy] is not changed here, use appPrefs to set it
|
||||
* */
|
||||
fun setNetCfg(cfg: NetCfg) {
|
||||
appPrefs.networkUseSocksProxy.set(cfg.useSocksProxy)
|
||||
@@ -3529,13 +3542,8 @@ data class NetCfg(
|
||||
val useSocksProxy: Boolean get() = socksProxy != null
|
||||
val enableKeepAlive: Boolean get() = tcpKeepAlive != null
|
||||
|
||||
fun withHostPort(hostPort: String?, default: String? = ":9050"): NetCfg {
|
||||
val socksProxy = if (hostPort?.startsWith("localhost:") == true) {
|
||||
hostPort.removePrefix("localhost")
|
||||
} else {
|
||||
hostPort ?: default
|
||||
}
|
||||
return copy(socksProxy = socksProxy)
|
||||
fun withProxy(proxy: NetworkProxy?, default: String? = ":9050"): NetCfg {
|
||||
return copy(socksProxy = proxy?.toProxyString() ?: default)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -3577,6 +3585,39 @@ data class NetCfg(
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class NetworkProxy(
|
||||
val username: String = "",
|
||||
val password: String = "",
|
||||
val auth: NetworkProxyAuth = NetworkProxyAuth.ISOLATE,
|
||||
val host: String = "localhost",
|
||||
val port: Int = 9050
|
||||
) {
|
||||
fun toProxyString(): String {
|
||||
var res = ""
|
||||
if (auth == NetworkProxyAuth.USERNAME && (username.isNotBlank() || password.isNotBlank())) {
|
||||
res += username.trim() + ":" + password.trim() + "@"
|
||||
} else if (auth == NetworkProxyAuth.USERNAME) {
|
||||
res += "@"
|
||||
}
|
||||
if (host != "localhost") {
|
||||
res += if (host.contains(':')) "[${host.trim(' ', '[', ']')}]" else host.trim()
|
||||
}
|
||||
if (port != 9050 || res.isEmpty()) {
|
||||
res += ":$port"
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class NetworkProxyAuth {
|
||||
@SerialName("isolate")
|
||||
ISOLATE,
|
||||
@SerialName("username")
|
||||
USERNAME,
|
||||
}
|
||||
|
||||
enum class OnionHosts {
|
||||
NEVER, PREFER, REQUIRED
|
||||
}
|
||||
@@ -5316,6 +5357,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}"
|
||||
@@ -6138,6 +6180,7 @@ enum class NotificationsMode() {
|
||||
@Serializable
|
||||
data class AppSettings(
|
||||
var networkConfig: NetCfg? = null,
|
||||
var networkProxy: NetworkProxy? = null,
|
||||
var privacyEncryptLocalFiles: Boolean? = null,
|
||||
var privacyAskToApproveRelays: Boolean? = null,
|
||||
var privacyAcceptImages: Boolean? = null,
|
||||
@@ -6169,6 +6212,7 @@ data class AppSettings(
|
||||
val empty = AppSettings()
|
||||
val def = defaults
|
||||
if (networkConfig != def.networkConfig) { empty.networkConfig = networkConfig }
|
||||
if (networkProxy != def.networkProxy) { empty.networkProxy = networkProxy }
|
||||
if (privacyEncryptLocalFiles != def.privacyEncryptLocalFiles) { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles }
|
||||
if (privacyAskToApproveRelays != def.privacyAskToApproveRelays) { empty.privacyAskToApproveRelays = privacyAskToApproveRelays }
|
||||
if (privacyAcceptImages != def.privacyAcceptImages) { empty.privacyAcceptImages = privacyAcceptImages }
|
||||
@@ -6206,8 +6250,12 @@ data class AppSettings(
|
||||
if (net.hostMode == HostMode.Onion) {
|
||||
net = net.copy(hostMode = HostMode.Public, requiredHostMode = true)
|
||||
}
|
||||
if (net.socksProxy != null) {
|
||||
net = net.copy(socksProxy = networkProxy?.toProxyString())
|
||||
}
|
||||
setNetCfg(net)
|
||||
}
|
||||
networkProxy?.let { def.networkProxy.set(it) }
|
||||
privacyEncryptLocalFiles?.let { def.privacyEncryptLocalFiles.set(it) }
|
||||
privacyAskToApproveRelays?.let { def.privacyAskToApproveRelays.set(it) }
|
||||
privacyAcceptImages?.let { def.privacyAcceptImages.set(it) }
|
||||
@@ -6240,6 +6288,7 @@ data class AppSettings(
|
||||
val defaults: AppSettings
|
||||
get() = AppSettings(
|
||||
networkConfig = NetCfg.defaults,
|
||||
networkProxy = null,
|
||||
privacyEncryptLocalFiles = true,
|
||||
privacyAskToApproveRelays = true,
|
||||
privacyAcceptImages = true,
|
||||
@@ -6273,6 +6322,7 @@ data class AppSettings(
|
||||
val def = appPreferences
|
||||
return defaults.copy(
|
||||
networkConfig = getNetCfg(),
|
||||
networkProxy = def.networkProxy.get(),
|
||||
privacyEncryptLocalFiles = def.privacyEncryptLocalFiles.get(),
|
||||
privacyAskToApproveRelays = def.privacyAskToApproveRelays.get(),
|
||||
privacyAcceptImages = def.privacyAcceptImages.get(),
|
||||
|
||||
+1
-1
@@ -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(
|
||||
|
||||
+1
@@ -47,6 +47,7 @@ class CallManager(val chatModel: ChatModel) {
|
||||
remoteHostId = invitation.remoteHostId,
|
||||
userProfile = userProfile,
|
||||
contact = invitation.contact,
|
||||
callUUID = invitation.callUUID,
|
||||
callState = CallState.InvitationAccepted,
|
||||
localMedia = invitation.callType.media,
|
||||
sharedKey = invitation.sharedKey,
|
||||
|
||||
+1
@@ -115,6 +115,7 @@ fun PreviewIncomingCallAlertLayout() {
|
||||
contact = Contact.sampleData,
|
||||
callType = CallType(media = CallMediaType.Audio, capabilities = CallCapabilities(encryption = false)),
|
||||
sharedKey = null,
|
||||
callUUID = "",
|
||||
callTs = Clock.System.now()
|
||||
),
|
||||
chatModel = ChatModel,
|
||||
|
||||
+2
@@ -13,6 +13,7 @@ data class Call(
|
||||
val remoteHostId: Long?,
|
||||
val userProfile: Profile,
|
||||
val contact: Contact,
|
||||
val callUUID: String?,
|
||||
val callState: CallState,
|
||||
val localMedia: CallMediaType,
|
||||
val localCapabilities: CallCapabilities? = null,
|
||||
@@ -105,6 +106,7 @@ sealed class WCallResponse {
|
||||
val contact: Contact,
|
||||
val callType: CallType,
|
||||
val sharedKey: String? = null,
|
||||
val callUUID: String,
|
||||
val callTs: Instant
|
||||
) {
|
||||
val callTypeText: String get() = generalGetString(when(callType.media) {
|
||||
|
||||
+10
-3
@@ -86,6 +86,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
.collect { chatId ->
|
||||
markUnreadChatAsRead(chatId)
|
||||
showSearch.value = false
|
||||
searchText.value = ""
|
||||
selectedChatItems.value = null
|
||||
}
|
||||
}
|
||||
@@ -104,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,
|
||||
@@ -141,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) {
|
||||
@@ -544,7 +551,7 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType)
|
||||
if (chatInfo is ChatInfo.Direct) {
|
||||
val contactInfo = chatModel.controller.apiContactInfo(remoteHostId, chatInfo.contact.contactId)
|
||||
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
|
||||
chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
||||
chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callUUID = null, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
||||
chatModel.showCallView.value = true
|
||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||
}
|
||||
@@ -997,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
|
||||
|
||||
+5
-6
@@ -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,
|
||||
|
||||
+2
-2
@@ -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) }
|
||||
)
|
||||
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -423,6 +423,7 @@ fun authStopChat(m: ChatModel, progressIndicator: MutableState<Boolean>? = null,
|
||||
authenticate(
|
||||
generalGetString(MR.strings.auth_stop_chat),
|
||||
generalGetString(MR.strings.auth_log_in_using_credential),
|
||||
oneTime = true,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success, is LAResult.Unavailable -> {
|
||||
|
||||
+2
-1
@@ -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),
|
||||
|
||||
+3
-1
@@ -34,6 +34,7 @@ expect fun authenticate(
|
||||
promptSubtitle: String,
|
||||
selfDestruct: Boolean = false,
|
||||
usingLAMode: LAMode = ChatModel.controller.appPrefs.laMode.get(),
|
||||
oneTime: Boolean,
|
||||
completed: (LAResult) -> Unit
|
||||
)
|
||||
|
||||
@@ -41,10 +42,11 @@ fun authenticateWithPasscode(
|
||||
promptTitle: String,
|
||||
promptSubtitle: String,
|
||||
selfDestruct: Boolean,
|
||||
oneTime: Boolean,
|
||||
completed: (LAResult) -> Unit
|
||||
) {
|
||||
val password = DatabaseUtils.ksAppPassword.get() ?: return completed(LAResult.Unavailable(generalGetString(MR.strings.la_no_app_password)))
|
||||
ModalManager.fullscreen.showPasscodeCustomModal { close ->
|
||||
ModalManager.fullscreen.showPasscodeCustomModal(oneTime) { close ->
|
||||
BackHandler {
|
||||
close()
|
||||
completed(LAResult.Error(generalGetString(MR.strings.authentication_cancelled)))
|
||||
|
||||
+13
-3
@@ -69,6 +69,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
// Don't use mutableStateOf() here, because it produces this if showing from SimpleXAPI.startChat():
|
||||
// java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied
|
||||
private var passcodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
|
||||
private var onTimePasscodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
|
||||
|
||||
fun showModal(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
|
||||
val data = ModalData()
|
||||
@@ -105,9 +106,13 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
}
|
||||
}
|
||||
|
||||
fun showPasscodeCustomModal(modal: @Composable (close: () -> Unit) -> Unit) {
|
||||
Log.d(TAG, "ModalManager.showPasscodeCustomModal")
|
||||
passcodeView.value = modal
|
||||
fun showPasscodeCustomModal(oneTime: Boolean, modal: @Composable (close: () -> Unit) -> Unit) {
|
||||
Log.d(TAG, "ModalManager.showPasscodeCustomModal, oneTime: $oneTime")
|
||||
if (oneTime) {
|
||||
onTimePasscodeView.value = modal
|
||||
} else {
|
||||
passcodeView.value = modal
|
||||
}
|
||||
}
|
||||
|
||||
fun hasModalsOpen() = modalCount.value > 0
|
||||
@@ -179,6 +184,11 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
passcodeView.collectAsState().value?.invoke { passcodeView.value = null }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun showOneTimePasscodeInView() {
|
||||
onTimePasscodeView.collectAsState().value?.invoke { onTimePasscodeView.value = null }
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to modify a list without getting [ConcurrentModificationException]
|
||||
* */
|
||||
|
||||
-6
@@ -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,
|
||||
|
||||
+19
-7
@@ -4,7 +4,6 @@ import SectionBottomSpacer
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
@@ -17,6 +16,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatController.getNetCfg
|
||||
import chat.simplex.common.model.ChatController.startChat
|
||||
import chat.simplex.common.model.ChatController.startChatWithTemporaryDatabase
|
||||
@@ -38,7 +38,6 @@ import kotlinx.serialization.*
|
||||
import java.io.File
|
||||
import java.net.URLEncoder
|
||||
import kotlin.math.max
|
||||
import kotlin.math.sqrt
|
||||
|
||||
@Serializable
|
||||
data class MigrationFileLinkData(
|
||||
@@ -46,16 +45,20 @@ data class MigrationFileLinkData(
|
||||
) {
|
||||
@Serializable
|
||||
data class NetworkConfig(
|
||||
val socksProxy: String?,
|
||||
// Legacy. Remove in 2025
|
||||
@SerialName("socksProxy")
|
||||
val legacySocksProxy: String?,
|
||||
val networkProxy: NetworkProxy?,
|
||||
val hostMode: HostMode?,
|
||||
val requiredHostMode: Boolean?
|
||||
) {
|
||||
fun hasOnionConfigured(): Boolean = socksProxy != null || hostMode == HostMode.Onion
|
||||
fun hasProxyConfigured(): Boolean = networkProxy != null || legacySocksProxy != null || hostMode == HostMode.Onion
|
||||
|
||||
fun transformToPlatformSupported(): NetworkConfig {
|
||||
return if (hostMode != null && requiredHostMode != null) {
|
||||
NetworkConfig(
|
||||
socksProxy = if (hostMode == HostMode.Onion) socksProxy ?: NetCfg.proxyDefaults.socksProxy else socksProxy,
|
||||
legacySocksProxy = if (hostMode == HostMode.Onion) legacySocksProxy ?: NetCfg.proxyDefaults.socksProxy else legacySocksProxy,
|
||||
networkProxy = if (hostMode == HostMode.Onion) networkProxy ?: NetworkProxy() else networkProxy,
|
||||
hostMode = if (hostMode == HostMode.Onion) HostMode.OnionViaSocks else hostMode,
|
||||
requiredHostMode = requiredHostMode
|
||||
)
|
||||
@@ -349,7 +352,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))
|
||||
@@ -562,7 +573,8 @@ private fun MutableState<MigrationFromState>.startUploading(
|
||||
val cfg = getNetCfg()
|
||||
val data = MigrationFileLinkData(
|
||||
networkConfig = MigrationFileLinkData.NetworkConfig(
|
||||
socksProxy = cfg.socksProxy,
|
||||
legacySocksProxy = null,
|
||||
networkProxy = if (appPrefs.networkUseSocksProxy.get()) appPrefs.networkProxy.get() else null,
|
||||
hostMode = cfg.hostMode,
|
||||
requiredHostMode = cfg.requiredHostMode
|
||||
)
|
||||
|
||||
+98
-87
@@ -5,16 +5,15 @@ import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.AppPreferences.Companion.SHARED_PREFS_MIGRATION_TO_STAGE
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatController.getNetCfg
|
||||
import chat.simplex.common.model.ChatController.startChat
|
||||
import chat.simplex.common.model.ChatCtrl
|
||||
@@ -41,10 +40,10 @@ import kotlin.math.max
|
||||
|
||||
@Serializable
|
||||
sealed class MigrationToDeviceState {
|
||||
@Serializable @SerialName("onion") data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToDeviceState()
|
||||
@Serializable @SerialName("downloadProgress") data class DownloadProgress(val link: String, val archiveName: String, val netCfg: NetCfg): MigrationToDeviceState()
|
||||
@Serializable @SerialName("archiveImport") data class ArchiveImport(val archiveName: String, val netCfg: NetCfg): MigrationToDeviceState()
|
||||
@Serializable @SerialName("passphrase") data class Passphrase(val netCfg: NetCfg): MigrationToDeviceState()
|
||||
@Serializable @SerialName("onion") data class Onion(val link: String, val socksProxy: String?, val networkProxy: NetworkProxy?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToDeviceState()
|
||||
@Serializable @SerialName("downloadProgress") data class DownloadProgress(val link: String, val archiveName: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToDeviceState()
|
||||
@Serializable @SerialName("archiveImport") data class ArchiveImport(val archiveName: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToDeviceState()
|
||||
@Serializable @SerialName("passphrase") data class Passphrase(val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToDeviceState()
|
||||
|
||||
companion object {
|
||||
// Here we check whether it's needed to show migration process after app restart or not
|
||||
@@ -66,10 +65,10 @@ sealed class MigrationToDeviceState {
|
||||
null
|
||||
} else {
|
||||
val archivePath = File(getMigrationTempFilesDirectory(), state.archiveName)
|
||||
MigrationToState.ArchiveImportFailed(archivePath.absolutePath, state.netCfg)
|
||||
MigrationToState.ArchiveImportFailed(archivePath.absolutePath, state.netCfg, state.networkProxy)
|
||||
}
|
||||
}
|
||||
is Passphrase -> MigrationToState.Passphrase("", state.netCfg)
|
||||
is Passphrase -> MigrationToState.Passphrase("", state.netCfg, state.networkProxy)
|
||||
}
|
||||
if (initial == null) {
|
||||
settings.remove(SHARED_PREFS_MIGRATION_TO_STAGE)
|
||||
@@ -91,16 +90,24 @@ sealed class MigrationToDeviceState {
|
||||
@Serializable
|
||||
sealed class MigrationToState {
|
||||
@Serializable object PasteOrScanLink: MigrationToState()
|
||||
@Serializable data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToState()
|
||||
@Serializable data class DatabaseInit(val link: String, val netCfg: NetCfg): MigrationToState()
|
||||
@Serializable data class LinkDownloading(val link: String, val ctrl: ChatCtrl, val user: User, val archivePath: String, val netCfg: NetCfg): MigrationToState()
|
||||
@Serializable data class DownloadProgress(val downloadedBytes: Long, val totalBytes: Long, val fileId: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val ctrl: ChatCtrl?): MigrationToState()
|
||||
@Serializable data class DownloadFailed(val totalBytes: Long, val link: String, val archivePath: String, val netCfg: NetCfg): MigrationToState()
|
||||
@Serializable data class ArchiveImport(val archivePath: String, val netCfg: NetCfg): MigrationToState()
|
||||
@Serializable data class ArchiveImportFailed(val archivePath: String, val netCfg: NetCfg): MigrationToState()
|
||||
@Serializable data class Passphrase(val passphrase: String, val netCfg: NetCfg): MigrationToState()
|
||||
@Serializable data class MigrationConfirmation(val status: DBMigrationResult, val passphrase: String, val useKeychain: Boolean, val netCfg: NetCfg): MigrationToState()
|
||||
@Serializable data class Migration(val passphrase: String, val confirmation: chat.simplex.common.views.helpers.MigrationConfirmation, val useKeychain: Boolean, val netCfg: NetCfg): MigrationToState()
|
||||
@Serializable data class Onion(
|
||||
val link: String,
|
||||
// Legacy, remove in 2025
|
||||
@SerialName("socksProxy")
|
||||
val legacySocksProxy: String?,
|
||||
val networkProxy: NetworkProxy?,
|
||||
val hostMode: HostMode,
|
||||
val requiredHostMode: Boolean
|
||||
): MigrationToState()
|
||||
@Serializable data class DatabaseInit(val link: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
|
||||
@Serializable data class LinkDownloading(val link: String, val ctrl: ChatCtrl, val user: User, val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
|
||||
@Serializable data class DownloadProgress(val downloadedBytes: Long, val totalBytes: Long, val fileId: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?, val ctrl: ChatCtrl?): MigrationToState()
|
||||
@Serializable data class DownloadFailed(val totalBytes: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
|
||||
@Serializable data class ArchiveImport(val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
|
||||
@Serializable data class ArchiveImportFailed(val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
|
||||
@Serializable data class Passphrase(val passphrase: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
|
||||
@Serializable data class MigrationConfirmation(val status: DBMigrationResult, val passphrase: String, val useKeychain: Boolean, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
|
||||
@Serializable data class Migration(val passphrase: String, val confirmation: chat.simplex.common.views.helpers.MigrationConfirmation, val useKeychain: Boolean, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
|
||||
}
|
||||
|
||||
private var MutableState<MigrationToState?>.state: MigrationToState?
|
||||
@@ -175,16 +182,16 @@ private fun ModalData.SectionByState(
|
||||
when (val s = migrationState.value) {
|
||||
null -> {}
|
||||
is MigrationToState.PasteOrScanLink -> migrationState.PasteOrScanLinkView()
|
||||
is MigrationToState.Onion -> OnionView(s.link, s.socksProxy, s.hostMode, s.requiredHostMode, migrationState)
|
||||
is MigrationToState.DatabaseInit -> migrationState.DatabaseInitView(s.link, tempDatabaseFile, s.netCfg)
|
||||
is MigrationToState.LinkDownloading -> migrationState.LinkDownloadingView(s.link, s.ctrl, s.user, s.archivePath, tempDatabaseFile, chatReceiver, s.netCfg)
|
||||
is MigrationToState.Onion -> OnionView(s.link, s.legacySocksProxy, s.networkProxy, s.hostMode, s.requiredHostMode, migrationState)
|
||||
is MigrationToState.DatabaseInit -> migrationState.DatabaseInitView(s.link, tempDatabaseFile, s.netCfg, s.networkProxy)
|
||||
is MigrationToState.LinkDownloading -> migrationState.LinkDownloadingView(s.link, s.ctrl, s.user, s.archivePath, tempDatabaseFile, chatReceiver, s.netCfg, s.networkProxy)
|
||||
is MigrationToState.DownloadProgress -> DownloadProgressView(s.downloadedBytes, totalBytes = s.totalBytes)
|
||||
is MigrationToState.DownloadFailed -> migrationState.DownloadFailedView(s.link, chatReceiver.value, s.archivePath, s.netCfg)
|
||||
is MigrationToState.ArchiveImport -> migrationState.ArchiveImportView(s.archivePath, s.netCfg)
|
||||
is MigrationToState.ArchiveImportFailed -> migrationState.ArchiveImportFailedView(s.archivePath, s.netCfg)
|
||||
is MigrationToState.Passphrase -> migrationState.PassphraseEnteringView(currentKey = s.passphrase, s.netCfg)
|
||||
is MigrationToState.MigrationConfirmation -> migrationState.MigrationConfirmationView(s.status, s.passphrase, s.useKeychain, s.netCfg)
|
||||
is MigrationToState.Migration -> MigrationView(s.passphrase, s.confirmation, s.useKeychain, s.netCfg, close)
|
||||
is MigrationToState.DownloadFailed -> migrationState.DownloadFailedView(s.link, chatReceiver.value, s.archivePath, s.netCfg, s.networkProxy)
|
||||
is MigrationToState.ArchiveImport -> migrationState.ArchiveImportView(s.archivePath, s.netCfg, s.networkProxy)
|
||||
is MigrationToState.ArchiveImportFailed -> migrationState.ArchiveImportFailedView(s.archivePath, s.netCfg, s.networkProxy)
|
||||
is MigrationToState.Passphrase -> migrationState.PassphraseEnteringView(currentKey = s.passphrase, s.netCfg, s.networkProxy)
|
||||
is MigrationToState.MigrationConfirmation -> migrationState.MigrationConfirmationView(s.status, s.passphrase, s.useKeychain, s.netCfg, s.networkProxy)
|
||||
is MigrationToState.Migration -> MigrationView(s.passphrase, s.confirmation, s.useKeychain, s.netCfg, s.networkProxy, close)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,10 +206,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,21 +223,24 @@ private fun MutableState<MigrationToState?>.PasteLinkView() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState<MigrationToState?>) {
|
||||
private fun ModalData.OnionView(link: String, legacyLinkSocksProxy: String?, linkNetworkProxy: NetworkProxy?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState<MigrationToState?>) {
|
||||
val onionHosts = remember { stateGetOrPut("onionHosts") {
|
||||
getNetCfg().copy(socksProxy = socksProxy, hostMode = hostMode, requiredHostMode = requiredHostMode).onionHosts
|
||||
getNetCfg().copy(socksProxy = linkNetworkProxy?.toProxyString() ?: legacyLinkSocksProxy, hostMode = hostMode, requiredHostMode = requiredHostMode).onionHosts
|
||||
} }
|
||||
val networkUseSocksProxy = remember { stateGetOrPut("networkUseSocksProxy") { socksProxy != null } }
|
||||
val networkUseSocksProxy = remember { stateGetOrPut("networkUseSocksProxy") { linkNetworkProxy != null || legacyLinkSocksProxy != null } }
|
||||
val sessionMode = remember { stateGetOrPut("sessionMode") { TransportSessionMode.User} }
|
||||
val networkProxyHostPort = remember { stateGetOrPut("networkHostProxyPort") {
|
||||
var proxy = (socksProxy ?: chatModel.controller.appPrefs.networkProxyHostPort.get())
|
||||
if (proxy?.startsWith(":") == true) proxy = "localhost$proxy"
|
||||
proxy
|
||||
}
|
||||
val networkProxy = remember { stateGetOrPut("networkProxy") {
|
||||
linkNetworkProxy
|
||||
?: if (legacyLinkSocksProxy != null) {
|
||||
NetworkProxy(host = legacyLinkSocksProxy.substringBefore(":").ifBlank { "localhost" }, port = legacyLinkSocksProxy.substringAfter(":").toIntOrNull() ?: 9050)
|
||||
} else {
|
||||
appPrefs.networkProxy.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val netCfg = rememberSaveable(stateSaver = serializableSaver()) {
|
||||
mutableStateOf(getNetCfg().withOnionHosts(onionHosts.value).copy(socksProxy = socksProxy, sessionMode = sessionMode.value))
|
||||
mutableStateOf(getNetCfg().withOnionHosts(onionHosts.value).copy(socksProxy = linkNetworkProxy?.toProxyString() ?: legacyLinkSocksProxy, sessionMode = sessionMode.value))
|
||||
}
|
||||
|
||||
SectionView(stringResource(MR.strings.migrate_to_device_confirm_network_settings).uppercase()) {
|
||||
@@ -243,12 +251,12 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
|
||||
click = {
|
||||
val updated = netCfg.value
|
||||
.withOnionHosts(onionHosts.value)
|
||||
.withHostPort(if (networkUseSocksProxy.value) networkProxyHostPort.value else null, null)
|
||||
.withProxy(if (networkUseSocksProxy.value) networkProxy.value else null, null)
|
||||
.copy(
|
||||
sessionMode = sessionMode.value
|
||||
)
|
||||
withBGApi {
|
||||
state.value = MigrationToState.DatabaseInit(link, updated)
|
||||
state.value = MigrationToState.DatabaseInit(link, updated, if (networkUseSocksProxy.value) networkProxy.value else null)
|
||||
}
|
||||
}
|
||||
){}
|
||||
@@ -257,8 +265,8 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
val networkProxyHostPortPref = SharedPreference(get = { networkProxyHostPort.value }, set = {
|
||||
networkProxyHostPort.value = it
|
||||
val networkProxyPref = SharedPreference(get = { networkProxy.value }, set = {
|
||||
networkProxy.value = it
|
||||
})
|
||||
SectionView(stringResource(MR.strings.network_settings_title).uppercase()) {
|
||||
OnionRelatedLayout(
|
||||
@@ -266,13 +274,10 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
|
||||
networkUseSocksProxy,
|
||||
onionHosts,
|
||||
sessionMode,
|
||||
networkProxyHostPortPref,
|
||||
networkProxyPref,
|
||||
toggleSocksProxy = { enable ->
|
||||
networkUseSocksProxy.value = enable
|
||||
},
|
||||
useOnion = {
|
||||
onionHosts.value = it
|
||||
},
|
||||
updateSessionMode = {
|
||||
sessionMode.value = it
|
||||
}
|
||||
@@ -281,13 +286,13 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState?>.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg) {
|
||||
private fun MutableState<MigrationToState?>.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg, networkProxy: NetworkProxy?) {
|
||||
Box {
|
||||
SectionView(stringResource(MR.strings.migrate_to_device_database_init).uppercase()) {}
|
||||
ProgressView()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
prepareDatabase(link, tempDatabaseFile, netCfg)
|
||||
prepareDatabase(link, tempDatabaseFile, netCfg, networkProxy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,14 +304,15 @@ private fun MutableState<MigrationToState?>.LinkDownloadingView(
|
||||
archivePath: String,
|
||||
tempDatabaseFile: File,
|
||||
chatReceiver: MutableState<MigrationToChatReceiver?>,
|
||||
netCfg: NetCfg
|
||||
netCfg: NetCfg,
|
||||
networkProxy: NetworkProxy?
|
||||
) {
|
||||
Box {
|
||||
SectionView(stringResource(MR.strings.migrate_to_device_downloading_details).uppercase()) {}
|
||||
ProgressView()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
startDownloading(0, ctrl, user, tempDatabaseFile, chatReceiver, link, archivePath, netCfg)
|
||||
startDownloading(0, ctrl, user, tempDatabaseFile, chatReceiver, link, archivePath, netCfg, networkProxy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,14 +327,14 @@ private fun DownloadProgressView(downloadedBytes: Long, totalBytes: Long) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState?>.DownloadFailedView(link: String, chatReceiver: MigrationToChatReceiver?, archivePath: String, netCfg: NetCfg) {
|
||||
private fun MutableState<MigrationToState?>.DownloadFailedView(link: String, chatReceiver: MigrationToChatReceiver?, archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
|
||||
SectionView(stringResource(MR.strings.migrate_to_device_download_failed).uppercase()) {
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_download),
|
||||
text = stringResource(MR.strings.migrate_to_device_repeat_download),
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
click = {
|
||||
state = MigrationToState.DatabaseInit(link, netCfg)
|
||||
state = MigrationToState.DatabaseInit(link, netCfg, networkProxy)
|
||||
}
|
||||
) {}
|
||||
SectionTextFooter(stringResource(MR.strings.migrate_to_device_try_again))
|
||||
@@ -341,25 +347,25 @@ private fun MutableState<MigrationToState?>.DownloadFailedView(link: String, cha
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState?>.ArchiveImportView(archivePath: String, netCfg: NetCfg) {
|
||||
private fun MutableState<MigrationToState?>.ArchiveImportView(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
|
||||
Box {
|
||||
SectionView(stringResource(MR.strings.migrate_to_device_importing_archive).uppercase()) {}
|
||||
ProgressView()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
importArchive(archivePath, netCfg)
|
||||
importArchive(archivePath, netCfg, networkProxy)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState?>.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg) {
|
||||
private fun MutableState<MigrationToState?>.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
|
||||
SectionView(stringResource(MR.strings.migrate_to_device_import_failed).uppercase()) {
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_download),
|
||||
text = stringResource(MR.strings.migrate_to_device_repeat_import),
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
click = {
|
||||
state = MigrationToState.ArchiveImport(archivePath, netCfg)
|
||||
state = MigrationToState.ArchiveImport(archivePath, netCfg, networkProxy)
|
||||
}
|
||||
) {}
|
||||
SectionTextFooter(stringResource(MR.strings.migrate_to_device_try_again))
|
||||
@@ -367,7 +373,7 @@ private fun MutableState<MigrationToState?>.ArchiveImportFailedView(archivePath:
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: String, netCfg: NetCfg) {
|
||||
private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
|
||||
val currentKey = rememberSaveable { mutableStateOf(currentKey) }
|
||||
val verifyingPassphrase = rememberSaveable { mutableStateOf(false) }
|
||||
val useKeychain = rememberSaveable { mutableStateOf(appPreferences.storeDBPassphrase.get()) }
|
||||
@@ -397,9 +403,9 @@ private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: S
|
||||
val (status, _) = chatInitTemporaryDatabase(dbAbsolutePrefixPath, key = currentKey.value, confirmation = MigrationConfirmation.YesUp)
|
||||
val success = status == DBMigrationResult.OK || status == DBMigrationResult.InvalidConfirmation
|
||||
if (success) {
|
||||
state = MigrationToState.Migration(currentKey.value, MigrationConfirmation.YesUp, useKeychain.value, netCfg)
|
||||
state = MigrationToState.Migration(currentKey.value, MigrationConfirmation.YesUp, useKeychain.value, netCfg, networkProxy)
|
||||
} else if (status is DBMigrationResult.ErrorMigration) {
|
||||
state = MigrationToState.MigrationConfirmation(status, currentKey.value, useKeychain.value, netCfg)
|
||||
state = MigrationToState.MigrationConfirmation(status, currentKey.value, useKeychain.value, netCfg, networkProxy)
|
||||
} else {
|
||||
showErrorOnMigrationIfNeeded(status)
|
||||
}
|
||||
@@ -416,7 +422,7 @@ private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: S
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg) {
|
||||
private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?) {
|
||||
data class Tuple4<A,B,C,D>(val a: A, val b: B, val c: C, val d: D)
|
||||
val (header: String, button: String?, footer: String, confirmation: MigrationConfirmation?) = when (status) {
|
||||
is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) {
|
||||
@@ -451,7 +457,7 @@ private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DB
|
||||
text = button,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
click = {
|
||||
state = MigrationToState.Migration(passphrase, confirmation, useKeychain, netCfg)
|
||||
state = MigrationToState.Migration(passphrase, confirmation, useKeychain, netCfg, networkProxy)
|
||||
}
|
||||
) {}
|
||||
}
|
||||
@@ -460,13 +466,13 @@ private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DB
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MigrationView(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, close: () -> Unit) {
|
||||
private fun MigrationView(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?, close: () -> Unit) {
|
||||
Box {
|
||||
SectionView(stringResource(MR.strings.migrate_to_device_migrating).uppercase()) {}
|
||||
ProgressView()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
startChat(passphrase, confirmation, useKeychain, netCfg, close)
|
||||
startChat(passphrase, confirmation, useKeychain, netCfg, networkProxy, close)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,19 +484,21 @@ private fun ProgressView() {
|
||||
private suspend fun MutableState<MigrationToState?>.checkUserLink(link: String) {
|
||||
if (strHasSimplexFileLink(link.trim())) {
|
||||
val data = MigrationFileLinkData.readFromLink(link)
|
||||
val hasOnionConfigured = data?.networkConfig?.hasOnionConfigured() ?: false
|
||||
val hasProxyConfigured = data?.networkConfig?.hasProxyConfigured() ?: false
|
||||
val networkConfig = data?.networkConfig?.transformToPlatformSupported()
|
||||
// If any of iOS or Android had onion enabled, show onion screen
|
||||
if (hasOnionConfigured && networkConfig?.hostMode != null && networkConfig.requiredHostMode != null) {
|
||||
state = MigrationToState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode)
|
||||
MigrationToDeviceState.save(MigrationToDeviceState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode))
|
||||
if (hasProxyConfigured && networkConfig?.hostMode != null && networkConfig.requiredHostMode != null) {
|
||||
state = MigrationToState.Onion(link.trim(), networkConfig.legacySocksProxy, networkConfig.networkProxy, networkConfig.hostMode, networkConfig.requiredHostMode)
|
||||
MigrationToDeviceState.save(MigrationToDeviceState.Onion(link.trim(), networkConfig.legacySocksProxy, networkConfig.networkProxy, networkConfig.hostMode, networkConfig.requiredHostMode))
|
||||
} else {
|
||||
val current = getNetCfg()
|
||||
state = MigrationToState.DatabaseInit(link.trim(), current.copy(
|
||||
socksProxy = networkConfig?.socksProxy,
|
||||
socksProxy = null,
|
||||
hostMode = networkConfig?.hostMode ?: current.hostMode,
|
||||
requiredHostMode = networkConfig?.requiredHostMode ?: current.requiredHostMode
|
||||
))
|
||||
),
|
||||
networkProxy = null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -504,6 +512,7 @@ private fun MutableState<MigrationToState?>.prepareDatabase(
|
||||
link: String,
|
||||
tempDatabaseFile: File,
|
||||
netCfg: NetCfg,
|
||||
networkProxy: NetworkProxy?
|
||||
) {
|
||||
withLongRunningApi {
|
||||
val ctrlAndUser = initTemporaryDatabase(tempDatabaseFile, netCfg)
|
||||
@@ -515,7 +524,7 @@ private fun MutableState<MigrationToState?>.prepareDatabase(
|
||||
}
|
||||
|
||||
val (ctrl, user) = ctrlAndUser
|
||||
state = MigrationToState.LinkDownloading(link, ctrl, user, archivePath(), netCfg)
|
||||
state = MigrationToState.LinkDownloading(link, ctrl, user, archivePath(), netCfg, networkProxy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,13 +537,14 @@ private fun MutableState<MigrationToState?>.startDownloading(
|
||||
link: String,
|
||||
archivePath: String,
|
||||
netCfg: NetCfg,
|
||||
networkProxy: NetworkProxy?
|
||||
) {
|
||||
withBGApi {
|
||||
chatReceiver.value = MigrationToChatReceiver(ctrl, tempDatabaseFile) { msg ->
|
||||
when (msg) {
|
||||
is CR.RcvFileProgressXFTP -> {
|
||||
state = MigrationToState.DownloadProgress(msg.receivedSize, msg.totalSize, msg.rcvFileTransfer.fileId, link, archivePath, netCfg, ctrl)
|
||||
MigrationToDeviceState.save(MigrationToDeviceState.DownloadProgress(link, File(archivePath).name, netCfg))
|
||||
state = MigrationToState.DownloadProgress(msg.receivedSize, msg.totalSize, msg.rcvFileTransfer.fileId, link, archivePath, netCfg, networkProxy, ctrl)
|
||||
MigrationToDeviceState.save(MigrationToDeviceState.DownloadProgress(link, File(archivePath).name, netCfg, networkProxy))
|
||||
}
|
||||
is CR.RcvStandaloneFileComplete -> {
|
||||
delay(500)
|
||||
@@ -542,8 +552,8 @@ private fun MutableState<MigrationToState?>.startDownloading(
|
||||
if (state == null) {
|
||||
MigrationToDeviceState.save(null)
|
||||
} else {
|
||||
state = MigrationToState.ArchiveImport(archivePath, netCfg)
|
||||
MigrationToDeviceState.save(MigrationToDeviceState.ArchiveImport(File(archivePath).name, netCfg))
|
||||
state = MigrationToState.ArchiveImport(archivePath, netCfg, networkProxy)
|
||||
MigrationToDeviceState.save(MigrationToDeviceState.ArchiveImport(File(archivePath).name, netCfg, networkProxy))
|
||||
}
|
||||
}
|
||||
is CR.RcvFileError -> {
|
||||
@@ -551,7 +561,7 @@ private fun MutableState<MigrationToState?>.startDownloading(
|
||||
generalGetString(MR.strings.migrate_to_device_download_failed),
|
||||
generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid)
|
||||
)
|
||||
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
|
||||
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy)
|
||||
}
|
||||
is CR.ChatRespError -> {
|
||||
if (msg.chatError is ChatError.ChatErrorChat && msg.chatError.errorType is ChatErrorType.NoRcvFileUser) {
|
||||
@@ -559,9 +569,9 @@ private fun MutableState<MigrationToState?>.startDownloading(
|
||||
generalGetString(MR.strings.migrate_to_device_download_failed),
|
||||
generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid)
|
||||
)
|
||||
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
|
||||
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy)
|
||||
} 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}")
|
||||
@@ -571,7 +581,7 @@ private fun MutableState<MigrationToState?>.startDownloading(
|
||||
|
||||
val (res, error) = controller.downloadStandaloneFile(user, link, CryptoFile.plain(File(archivePath).path), ctrl)
|
||||
if (res == null) {
|
||||
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
|
||||
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy)
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.migrate_to_device_error_downloading_archive),
|
||||
error
|
||||
@@ -580,7 +590,7 @@ private fun MutableState<MigrationToState?>.startDownloading(
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableState<MigrationToState?>.importArchive(archivePath: String, netCfg: NetCfg) {
|
||||
private fun MutableState<MigrationToState?>.importArchive(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
|
||||
withLongRunningApi {
|
||||
try {
|
||||
if (ChatController.ctrl == null || ChatController.ctrl == -1L) {
|
||||
@@ -594,14 +604,14 @@ private fun MutableState<MigrationToState?>.importArchive(archivePath: String, n
|
||||
if (archiveErrors.isNotEmpty()) {
|
||||
showArchiveImportedWithErrorsAlert(archiveErrors)
|
||||
}
|
||||
state = MigrationToState.Passphrase("", netCfg)
|
||||
MigrationToDeviceState.save(MigrationToDeviceState.Passphrase(netCfg))
|
||||
state = MigrationToState.Passphrase("", netCfg, networkProxy)
|
||||
MigrationToDeviceState.save(MigrationToDeviceState.Passphrase(netCfg, networkProxy))
|
||||
} catch (e: Exception) {
|
||||
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg)
|
||||
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg, networkProxy)
|
||||
AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_importing_database), e.stackTraceToString())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg)
|
||||
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg, networkProxy)
|
||||
AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_deleting_database), e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
@@ -611,7 +621,7 @@ private suspend fun stopArchiveDownloading(fileId: Long, ctrl: ChatCtrl) {
|
||||
controller.apiCancelFile(null, fileId, ctrl)
|
||||
}
|
||||
|
||||
private fun startChat(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, close: () -> Unit) {
|
||||
private fun startChat(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?, close: () -> Unit) {
|
||||
if (useKeychain) {
|
||||
ksDatabasePassword.set(passphrase)
|
||||
} else {
|
||||
@@ -623,7 +633,8 @@ private fun startChat(passphrase: String, confirmation: MigrationConfirmation, u
|
||||
try {
|
||||
initChatController(useKey = passphrase, confirmMigrations = confirmation) { CompletableDeferred(false) }
|
||||
val appSettings = controller.apiGetAppSettings(AppSettings.current.prepareForExport()).copy(
|
||||
networkConfig = netCfg
|
||||
networkConfig = netCfg,
|
||||
networkProxy = networkProxy
|
||||
)
|
||||
finishMigration(appSettings, close)
|
||||
} catch (e: Exception) {
|
||||
|
||||
+136
-121
@@ -1,7 +1,9 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import SectionDivider
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import TextIconSpaced
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
@@ -34,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 } }
|
||||
@@ -95,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
|
||||
@@ -109,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,
|
||||
@@ -120,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) }
|
||||
@@ -164,6 +182,10 @@ private fun NewChatSheetLayout(
|
||||
)
|
||||
|
||||
val sectionModifier = Modifier.fillMaxWidth()
|
||||
val deletedContactTypes = listOf(ContactType.CHAT_DELETED)
|
||||
val deletedChats by remember(chatModel.chats.value, deletedContactTypes) {
|
||||
derivedStateOf { filterContactTypes(chatModel.chats.value, deletedContactTypes) }
|
||||
}
|
||||
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
@@ -224,7 +246,9 @@ private fun NewChatSheetLayout(
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(Modifier.padding(bottom = 27.dp))
|
||||
if (searchText.value.text.isEmpty()) {
|
||||
Spacer(Modifier.padding(bottom = 27.dp))
|
||||
}
|
||||
|
||||
val actionButtonsOriginal = listOf(
|
||||
Triple(
|
||||
@@ -262,37 +286,30 @@ private fun NewChatSheetLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val deletedContactTypes = listOf(ContactType.CHAT_DELETED)
|
||||
val deletedChats by remember(chatModel.chats.value, deletedContactTypes) {
|
||||
derivedStateOf { filterContactTypes(chatModel.chats.value, deletedContactTypes) }
|
||||
}
|
||||
if (deletedChats.isNotEmpty()) {
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
Row(modifier = sectionModifier) {
|
||||
SectionView {
|
||||
SectionItemView(
|
||||
click = {
|
||||
ModalManager.start.showCustomModal { closeDeletedChats ->
|
||||
ModalView(
|
||||
close = closeDeletedChats,
|
||||
closeOnTop = !oneHandUI.value,
|
||||
) {
|
||||
DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = {
|
||||
ModalManager.start.closeModals()
|
||||
})
|
||||
}
|
||||
SectionView {
|
||||
SectionItemView(
|
||||
click = {
|
||||
ModalManager.start.showCustomModal { closeDeletedChats ->
|
||||
ModalView(
|
||||
close = closeDeletedChats,
|
||||
closeOnTop = !oneHandUI.value,
|
||||
) {
|
||||
DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = {
|
||||
ModalManager.start.closeModals()
|
||||
})
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_inventory_2),
|
||||
contentDescription = stringResource(MR.strings.deleted_chats),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
)
|
||||
TextIconSpaced(false)
|
||||
Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_inventory_2),
|
||||
contentDescription = stringResource(MR.strings.deleted_chats),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
)
|
||||
TextIconSpaced(false)
|
||||
Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,16 +317,28 @@ private fun NewChatSheetLayout(
|
||||
}
|
||||
|
||||
item {
|
||||
if (filteredContactChats.isNotEmpty() && !oneHandUI.value) {
|
||||
if (searchText.value.text.isNotEmpty()) {
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
} else {
|
||||
if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) {
|
||||
if (!oneHandUI.value) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {}
|
||||
} else {
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
|
||||
SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {}
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
|
||||
Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
generalGetString(MR.strings.no_filtered_contacts),
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
stringResource(MR.strings.contact_list_header_title).uppercase(), color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2,
|
||||
modifier = sectionModifier.padding(start = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF), fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,17 +351,6 @@ private fun NewChatSheetLayout(
|
||||
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true)
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
|
||||
Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
generalGetString(MR.strings.no_filtered_contacts),
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -534,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 } }
|
||||
@@ -555,78 +573,75 @@ private fun DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats: () -> Un
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it)
|
||||
) { contentPadding ->
|
||||
val listState = remember { appBarHandler.listState }
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||
val searchShowingSimplexLink = remember { mutableStateOf(false) }
|
||||
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
|
||||
val showUnreadAndFavorites = remember { appPrefs.showUnreadAndFavorites.state }.value
|
||||
val allChats by remember(chatModel.chats.value) {
|
||||
derivedStateOf { filterContactTypes(chatModel.chats.value, listOf(ContactType.CHAT_DELETED)) }
|
||||
}
|
||||
val filteredContactChats = filteredContactChats(
|
||||
showUnreadAndFavorites = showUnreadAndFavorites,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchText = searchText.value.text,
|
||||
contactChats = allChats
|
||||
)
|
||||
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
contentPadding = contentPadding,
|
||||
reverseLayout = oneHandUI.value,
|
||||
) {
|
||||
if (!oneHandUI.value) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
stringResource(MR.strings.deleted_chats),
|
||||
hostDevice(rh?.remoteHostId),
|
||||
bottomPadding = bottomPadding
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||
val searchShowingSimplexLink = remember { mutableStateOf(false) }
|
||||
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
|
||||
val showUnreadAndFavorites = remember { appPrefs.showUnreadAndFavorites.state }.value
|
||||
val allChats by remember(chatModel.chats.value) {
|
||||
derivedStateOf { filterContactTypes(chatModel.chats.value, listOf(ContactType.CHAT_DELETED)) }
|
||||
}
|
||||
val filteredContactChats = filteredContactChats(
|
||||
showUnreadAndFavorites = showUnreadAndFavorites,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchText = searchText.value.text,
|
||||
contactChats = allChats
|
||||
)
|
||||
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
reverseLayout = oneHandUI.value,
|
||||
) {
|
||||
item {
|
||||
if (!oneHandUI.value) {
|
||||
Divider()
|
||||
}
|
||||
ContactsSearchBar(
|
||||
listState = listState,
|
||||
searchText = searchText,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
close = close,
|
||||
)
|
||||
Divider()
|
||||
|
||||
Spacer(Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
}
|
||||
|
||||
itemsIndexed(filteredContactChats) { index, chat ->
|
||||
val nextChatSelected = remember(chat.id, filteredContactChats) {
|
||||
derivedStateOf {
|
||||
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
}
|
||||
}
|
||||
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = false)
|
||||
}
|
||||
}
|
||||
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
|
||||
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
generalGetString(MR.strings.no_filtered_contacts),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
item {
|
||||
if (!oneHandUI.value) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
stringResource(MR.strings.deleted_chats),
|
||||
hostDevice(rh?.remoteHostId),
|
||||
bottomPadding = bottomPadding
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
if (!oneHandUI.value) {
|
||||
Divider()
|
||||
}
|
||||
ContactsSearchBar(
|
||||
listState = listState,
|
||||
searchText = searchText,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
close = close,
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
|
||||
item {
|
||||
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
|
||||
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
generalGetString(MR.strings.no_filtered_contacts),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(filteredContactChats) { index, chat ->
|
||||
val nextChatSelected = remember(chat.id, filteredContactChats) {
|
||||
derivedStateOf {
|
||||
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
}
|
||||
}
|
||||
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -724,6 +739,6 @@ fun ActionButton(
|
||||
@Composable
|
||||
private fun PreviewNewChatSheet() {
|
||||
SimpleXTheme {
|
||||
NewChatSheet(rh = null, close = {})
|
||||
ModalData().NewChatSheet(rh = null, close = {})
|
||||
}
|
||||
}
|
||||
|
||||
+3
-29
@@ -1,10 +1,10 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionCustomFooter
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionItemWithValue
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import SectionViewSelectableCards
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
@@ -40,12 +40,10 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
|
||||
val currentCfg = remember { stateGetOrPut("currentCfg") { controller.getNetCfg() } }
|
||||
val currentCfgVal = currentCfg.value // used only on initialization
|
||||
|
||||
val onionHosts = remember { mutableStateOf(currentCfgVal.onionHosts) }
|
||||
val sessionMode = remember { mutableStateOf(currentCfgVal.sessionMode) }
|
||||
val smpProxyMode = remember { mutableStateOf(currentCfgVal.smpProxyMode) }
|
||||
val smpProxyFallback = remember { mutableStateOf(currentCfgVal.smpProxyFallback) }
|
||||
|
||||
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(currentCfgVal.useSocksProxy) }
|
||||
val networkTCPConnectTimeout = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout) }
|
||||
val networkTCPTimeout = remember { mutableStateOf(currentCfgVal.tcpTimeout) }
|
||||
val networkTCPTimeoutPerKb = remember { mutableStateOf(currentCfgVal.tcpTimeoutPerKb) }
|
||||
@@ -90,11 +88,10 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
|
||||
tcpKeepAlive = tcpKeepAlive,
|
||||
smpPingInterval = networkSMPPingInterval.value,
|
||||
smpPingCount = networkSMPPingCount.value
|
||||
).withOnionHosts(onionHosts.value)
|
||||
).withOnionHosts(currentCfg.value.onionHosts)
|
||||
}
|
||||
|
||||
fun updateView(cfg: NetCfg) {
|
||||
onionHosts.value = cfg.onionHosts
|
||||
sessionMode.value = cfg.sessionMode
|
||||
smpProxyMode.value = cfg.smpProxyMode
|
||||
smpProxyFallback.value = cfg.smpProxyFallback
|
||||
@@ -148,10 +145,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
|
||||
) {
|
||||
AdvancedNetworkSettingsLayout(
|
||||
currentRemoteHost = currentRemoteHost,
|
||||
networkUseSocksProxy = networkUseSocksProxy,
|
||||
developerTools = developerTools,
|
||||
onionHosts = onionHosts,
|
||||
useOnion = { onionHosts.value = it; currentCfg.value = currentCfg.value.withOnionHosts(it) },
|
||||
sessionMode = sessionMode,
|
||||
smpProxyMode = smpProxyMode,
|
||||
smpProxyFallback = smpProxyFallback,
|
||||
@@ -183,10 +177,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
|
||||
|
||||
@Composable fun AdvancedNetworkSettingsLayout(
|
||||
currentRemoteHost: RemoteHostInfo?,
|
||||
networkUseSocksProxy: State<Boolean>,
|
||||
developerTools: Boolean,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
useOnion: (OnionHosts) -> Unit,
|
||||
sessionMode: MutableState<TransportSessionMode>,
|
||||
smpProxyMode: MutableState<SMPProxyMode>,
|
||||
smpProxyFallback: MutableState<SMPProxyFallback>,
|
||||
@@ -223,21 +214,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
|
||||
SMPProxyFallbackPicker(smpProxyFallback, showModal, updateSMPProxyFallback, enabled = remember { derivedStateOf { smpProxyMode.value != SMPProxyMode.Never } })
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_arrow_forward), stringResource(MR.strings.private_routing_show_message_status), chatModel.controller.appPrefs.showSentViaProxy)
|
||||
}
|
||||
SectionCustomFooter {
|
||||
Text(stringResource(MR.strings.private_routing_explanation))
|
||||
}
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
}
|
||||
|
||||
if (currentRemoteHost == null && networkUseSocksProxy.value) {
|
||||
SectionView(stringResource(MR.strings.network_socks_proxy).uppercase()) {
|
||||
UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion)
|
||||
SectionCustomFooter {
|
||||
Column {
|
||||
Text(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported))
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionTextFooter(stringResource(MR.strings.private_routing_explanation))
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
}
|
||||
|
||||
@@ -562,7 +539,6 @@ fun PreviewAdvancedNetworkSettingsLayout() {
|
||||
SimpleXTheme {
|
||||
AdvancedNetworkSettingsLayout(
|
||||
currentRemoteHost = null,
|
||||
networkUseSocksProxy = remember { mutableStateOf(false) },
|
||||
developerTools = false,
|
||||
sessionMode = remember { mutableStateOf(TransportSessionMode.User) },
|
||||
smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) },
|
||||
@@ -577,8 +553,6 @@ fun PreviewAdvancedNetworkSettingsLayout() {
|
||||
networkTCPKeepIdle = remember { mutableStateOf(10) },
|
||||
networkTCPKeepIntvl = remember { mutableStateOf(10) },
|
||||
networkTCPKeepCnt = remember { mutableStateOf(10) },
|
||||
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
|
||||
useOnion = {},
|
||||
updateSessionMode = {},
|
||||
updateSMPProxyMode = {},
|
||||
updateSMPProxyFallback = {},
|
||||
|
||||
+189
-96
@@ -1,10 +1,10 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionCustomFooter
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionItemWithValue
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import SectionViewSelectable
|
||||
import TextIconSpaced
|
||||
@@ -37,10 +37,11 @@ fun NetworkAndServersView() {
|
||||
val netCfg = remember { chatModel.controller.getNetCfg() }
|
||||
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(netCfg.useSocksProxy) }
|
||||
|
||||
val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } }
|
||||
val proxyPort = remember { derivedStateOf { appPrefs.networkProxy.state.value.port } }
|
||||
NetworkAndServersLayout(
|
||||
currentRemoteHost = currentRemoteHost,
|
||||
networkUseSocksProxy = networkUseSocksProxy,
|
||||
onionHosts = remember { mutableStateOf(netCfg.onionHosts) },
|
||||
toggleSocksProxy = { enable ->
|
||||
val def = NetCfg.defaults
|
||||
val proxyDef = NetCfg.proxyDefaults
|
||||
@@ -51,7 +52,7 @@ fun NetworkAndServersView() {
|
||||
confirmText = generalGetString(MR.strings.confirm_verb),
|
||||
onConfirm = {
|
||||
withBGApi {
|
||||
var conf = controller.getNetCfg().withHostPort(controller.appPrefs.networkProxyHostPort.get())
|
||||
var conf = controller.getNetCfg().withProxy(controller.appPrefs.networkProxy.get())
|
||||
if (conf.tcpConnectTimeout == def.tcpConnectTimeout) {
|
||||
conf = conf.copy(tcpConnectTimeout = proxyDef.tcpConnectTimeout)
|
||||
}
|
||||
@@ -104,6 +105,7 @@ fun NetworkAndServersView() {
|
||||
@Composable fun NetworkAndServersLayout(
|
||||
currentRemoteHost: RemoteHostInfo?,
|
||||
networkUseSocksProxy: MutableState<Boolean>,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
toggleSocksProxy: (Boolean) -> Unit,
|
||||
) {
|
||||
val m = chatModel
|
||||
@@ -120,14 +122,10 @@ fun NetworkAndServersView() {
|
||||
|
||||
if (currentRemoteHost == null) {
|
||||
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
|
||||
SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, appPrefs.networkProxyHostPort, false, it) }})
|
||||
SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, appPrefs.networkProxy, onionHosts, sessionMode = appPrefs.networkSessionMode.get(), false, it) }})
|
||||
SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), { ModalManager.start.showCustomModal { AdvancedNetworkSettingsView(showModal, it) } })
|
||||
if (networkUseSocksProxy.value) {
|
||||
SectionCustomFooter {
|
||||
Column {
|
||||
Text(annotatedStringResource(MR.strings.socks_proxy_setting_limitations))
|
||||
}
|
||||
}
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.socks_proxy_setting_limitations))
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
} else {
|
||||
SectionDividerSpaced()
|
||||
@@ -158,16 +156,14 @@ fun NetworkAndServersView() {
|
||||
networkUseSocksProxy: MutableState<Boolean>,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
sessionMode: MutableState<TransportSessionMode>,
|
||||
networkProxyHostPort: SharedPreference<String?>,
|
||||
networkProxy: SharedPreference<NetworkProxy>,
|
||||
toggleSocksProxy: (Boolean) -> Unit,
|
||||
useOnion: (OnionHosts) -> Unit,
|
||||
updateSessionMode: (TransportSessionMode) -> Unit,
|
||||
) {
|
||||
val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.fullscreen.showModal(content = it) }
|
||||
val showCustomModal = { it: @Composable (close: () -> Unit) -> Unit -> ModalManager.fullscreen.showCustomModal { close -> it(close) }}
|
||||
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
|
||||
SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, networkProxyHostPort, true, it) } })
|
||||
UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion)
|
||||
SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, networkProxy, onionHosts, sessionMode.value, true, it) } })
|
||||
if (developerTools) {
|
||||
SessionModePicker(sessionMode, showModal, updateSessionMode)
|
||||
}
|
||||
@@ -205,46 +201,98 @@ fun UseSocksProxySwitch(
|
||||
@Composable
|
||||
fun SocksProxySettings(
|
||||
networkUseSocksProxy: Boolean,
|
||||
networkProxyHostPort: SharedPreference<String?> = appPrefs.networkProxyHostPort,
|
||||
networkProxy: SharedPreference<NetworkProxy>,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
sessionMode: TransportSessionMode,
|
||||
migration: Boolean,
|
||||
close: () -> Unit
|
||||
) {
|
||||
val defaultHostPort = remember { "localhost:9050" }
|
||||
val hostPortSaved by remember { networkProxyHostPort.state }
|
||||
val networkProxySaved by remember { networkProxy.state }
|
||||
val onionHostsSaved = remember { mutableStateOf(onionHosts.value) }
|
||||
|
||||
val usernameUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(networkProxySaved.username))
|
||||
}
|
||||
val passwordUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(networkProxySaved.password))
|
||||
}
|
||||
val hostUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.firstOrNull() ?: "localhost"))
|
||||
mutableStateOf(TextFieldValue(networkProxySaved.host))
|
||||
}
|
||||
val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.lastOrNull() ?: "9050"))
|
||||
mutableStateOf(TextFieldValue(networkProxySaved.port.toString()))
|
||||
}
|
||||
val save = {
|
||||
val oldValue = networkProxyHostPort.get()
|
||||
networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
|
||||
if (networkUseSocksProxy && !migration) {
|
||||
withBGApi {
|
||||
if (!controller.apiSetNetworkConfig(controller.getNetCfg())) {
|
||||
networkProxyHostPort.set(oldValue)
|
||||
}
|
||||
val proxyAuthRandomUnsaved = rememberSaveable { mutableStateOf(networkProxySaved.auth == NetworkProxyAuth.ISOLATE) }
|
||||
LaunchedEffect(proxyAuthRandomUnsaved.value) {
|
||||
if (!proxyAuthRandomUnsaved.value && onionHosts.value != OnionHosts.NEVER) {
|
||||
onionHosts.value = OnionHosts.NEVER
|
||||
}
|
||||
}
|
||||
val proxyAuthModeUnsaved = remember(proxyAuthRandomUnsaved.value, usernameUnsaved.value.text, passwordUnsaved.value.text) {
|
||||
derivedStateOf {
|
||||
if (proxyAuthRandomUnsaved.value) {
|
||||
NetworkProxyAuth.ISOLATE
|
||||
} else {
|
||||
NetworkProxyAuth.USERNAME
|
||||
}
|
||||
}
|
||||
}
|
||||
val saveAndClose = {
|
||||
val oldValue = networkProxyHostPort.get()
|
||||
networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
|
||||
|
||||
val save: (Boolean) -> Unit = { closeOnSuccess ->
|
||||
val oldValue = networkProxy.get()
|
||||
usernameUnsaved.value = usernameUnsaved.value.copy(if (proxyAuthModeUnsaved.value == NetworkProxyAuth.USERNAME) usernameUnsaved.value.text.trim() else "")
|
||||
passwordUnsaved.value = passwordUnsaved.value.copy(if (proxyAuthModeUnsaved.value == NetworkProxyAuth.USERNAME) passwordUnsaved.value.text.trim() else "")
|
||||
hostUnsaved.value = hostUnsaved.value.copy(hostUnsaved.value.text.trim())
|
||||
portUnsaved.value = portUnsaved.value.copy(portUnsaved.value.text.trim())
|
||||
|
||||
networkProxy.set(
|
||||
NetworkProxy(
|
||||
username = usernameUnsaved.value.text,
|
||||
password = passwordUnsaved.value.text,
|
||||
host = hostUnsaved.value.text,
|
||||
port = portUnsaved.value.text.toIntOrNull() ?: 9050,
|
||||
auth = proxyAuthModeUnsaved.value
|
||||
)
|
||||
)
|
||||
val oldCfg = controller.getNetCfg()
|
||||
val cfg = oldCfg.withOnionHosts(onionHosts.value)
|
||||
val oldOnionHosts = onionHostsSaved.value
|
||||
onionHostsSaved.value = onionHosts.value
|
||||
|
||||
if (!migration) {
|
||||
controller.setNetCfg(cfg)
|
||||
}
|
||||
if (networkUseSocksProxy && !migration) {
|
||||
withBGApi {
|
||||
if (controller.apiSetNetworkConfig(controller.getNetCfg())) {
|
||||
close()
|
||||
if (controller.apiSetNetworkConfig(cfg, showAlertOnError = false)) {
|
||||
onionHosts.value = cfg.onionHosts
|
||||
onionHostsSaved.value = onionHosts.value
|
||||
if (closeOnSuccess) {
|
||||
close()
|
||||
}
|
||||
} else {
|
||||
networkProxyHostPort.set(oldValue)
|
||||
controller.setNetCfg(oldCfg)
|
||||
networkProxy.set(oldValue)
|
||||
onionHostsSaved.value = oldOnionHosts
|
||||
showWrongProxyConfigAlert()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val saveDisabled = hostPortSaved == (hostUnsaved.value.text + ":" + portUnsaved.value.text) ||
|
||||
remember { derivedStateOf { !validHost(hostUnsaved.value.text) } }.value ||
|
||||
remember { derivedStateOf { !validPort(portUnsaved.value.text) } }.value
|
||||
val resetDisabled = hostUnsaved.value.text + ":" + portUnsaved.value.text == defaultHostPort
|
||||
val saveDisabled =
|
||||
(
|
||||
networkProxySaved.username == usernameUnsaved.value.text.trim() &&
|
||||
networkProxySaved.password == passwordUnsaved.value.text.trim() &&
|
||||
networkProxySaved.host == hostUnsaved.value.text.trim() &&
|
||||
networkProxySaved.port.toString() == portUnsaved.value.text.trim() &&
|
||||
networkProxySaved.auth == proxyAuthModeUnsaved.value &&
|
||||
onionHosts.value == onionHostsSaved.value
|
||||
) ||
|
||||
!validCredential(usernameUnsaved.value.text) ||
|
||||
!validCredential(passwordUnsaved.value.text) ||
|
||||
!validHost(hostUnsaved.value.text) ||
|
||||
!validPort(portUnsaved.value.text)
|
||||
val resetDisabled = hostUnsaved.value.text.trim() == "localhost" && portUnsaved.value.text.trim() == "9050" && proxyAuthRandomUnsaved.value && onionHosts.value == NetCfg.defaults.onionHosts
|
||||
ModalView(
|
||||
close = {
|
||||
if (saveDisabled) {
|
||||
@@ -252,7 +300,7 @@ fun SocksProxySettings(
|
||||
} else {
|
||||
showUnsavedSocksHostPortAlert(
|
||||
confirmText = generalGetString(if (networkUseSocksProxy && !migration) MR.strings.network_options_save_and_reconnect else MR.strings.network_options_save),
|
||||
save = saveAndClose,
|
||||
save = { save(true) },
|
||||
close = close
|
||||
)
|
||||
}
|
||||
@@ -263,38 +311,78 @@ fun SocksProxySettings(
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings))
|
||||
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
DefaultConfigurableTextField(
|
||||
hostUnsaved,
|
||||
stringResource(MR.strings.host_verb),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isValid = ::validHost,
|
||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
DefaultConfigurableTextField(
|
||||
portUnsaved,
|
||||
stringResource(MR.strings.port_verb),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isValid = ::validPort,
|
||||
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save() }),
|
||||
keyboardType = KeyboardType.Number,
|
||||
)
|
||||
SectionView(stringResource(MR.strings.network_socks_proxy).uppercase()) {
|
||||
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||
DefaultConfigurableTextField(
|
||||
hostUnsaved,
|
||||
stringResource(MR.strings.host_verb),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isValid = ::validHost,
|
||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
DefaultConfigurableTextField(
|
||||
portUnsaved,
|
||||
stringResource(MR.strings.port_verb),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isValid = ::validPort,
|
||||
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save(false) }),
|
||||
keyboardType = KeyboardType.Number,
|
||||
)
|
||||
}
|
||||
|
||||
UseOnionHosts(onionHosts, rememberUpdatedState(networkUseSocksProxy && proxyAuthRandomUnsaved.value)) {
|
||||
onionHosts.value = it
|
||||
}
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported))
|
||||
}
|
||||
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
|
||||
SectionView(stringResource(MR.strings.network_proxy_auth).uppercase()) {
|
||||
PreferenceToggle(
|
||||
stringResource(MR.strings.network_proxy_random_credentials),
|
||||
checked = proxyAuthRandomUnsaved.value,
|
||||
onChange = { proxyAuthRandomUnsaved.value = it }
|
||||
)
|
||||
if (!proxyAuthRandomUnsaved.value) {
|
||||
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||
DefaultConfigurableTextField(
|
||||
usernameUnsaved,
|
||||
stringResource(MR.strings.network_proxy_username),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isValid = ::validCredential,
|
||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
DefaultConfigurableTextField(
|
||||
passwordUnsaved,
|
||||
stringResource(MR.strings.network_proxy_password),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isValid = ::validCredential,
|
||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||
keyboardType = KeyboardType.Password,
|
||||
)
|
||||
}
|
||||
}
|
||||
SectionTextFooter(proxyAuthFooter(usernameUnsaved.value.text, passwordUnsaved.value.text, proxyAuthModeUnsaved.value, sessionMode))
|
||||
}
|
||||
|
||||
SectionDividerSpaced(maxBottomPadding = false, maxTopPadding = true)
|
||||
|
||||
SectionView {
|
||||
SectionItemView({
|
||||
val newHost = defaultHostPort.split(":").first()
|
||||
val newPort = defaultHostPort.split(":").last()
|
||||
hostUnsaved.value = hostUnsaved.value.copy(newHost, TextRange(newHost.length))
|
||||
portUnsaved.value = portUnsaved.value.copy(newPort, TextRange(newPort.length))
|
||||
hostUnsaved.value = hostUnsaved.value.copy("localhost", TextRange(9))
|
||||
portUnsaved.value = portUnsaved.value.copy("9050", TextRange(4))
|
||||
usernameUnsaved.value = TextFieldValue()
|
||||
passwordUnsaved.value = TextFieldValue()
|
||||
proxyAuthRandomUnsaved.value = true
|
||||
onionHosts.value = NetCfg.defaults.onionHosts
|
||||
}, disabled = resetDisabled) {
|
||||
Text(stringResource(MR.strings.network_options_reset_to_defaults), color = if (resetDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView(
|
||||
click = { if (networkUseSocksProxy && !migration) showUpdateNetworkSettingsDialog { save() } else save() },
|
||||
click = { if (networkUseSocksProxy && !migration) showUpdateNetworkSettingsDialog { save(false) } else save(false) },
|
||||
disabled = saveDisabled
|
||||
) {
|
||||
Text(stringResource(if (networkUseSocksProxy && !migration) MR.strings.network_options_save_and_reconnect else MR.strings.network_options_save), color = if (saveDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
|
||||
@@ -305,6 +393,12 @@ fun SocksProxySettings(
|
||||
}
|
||||
}
|
||||
|
||||
private fun proxyAuthFooter(username: String, password: String, auth: NetworkProxyAuth, sessionMode: TransportSessionMode): String = when {
|
||||
auth == NetworkProxyAuth.ISOLATE -> generalGetString(if (sessionMode == TransportSessionMode.User) MR.strings.network_proxy_auth_mode_isolate_by_auth_user else MR.strings.network_proxy_auth_mode_isolate_by_auth_entity)
|
||||
username.isBlank() && password.isBlank() -> generalGetString(MR.strings.network_proxy_auth_mode_no_auth)
|
||||
else -> generalGetString(MR.strings.network_proxy_auth_mode_username_password)
|
||||
}
|
||||
|
||||
private fun showUnsavedSocksHostPortAlert(confirmText: String, save: () -> Unit, close: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(MR.strings.update_network_settings_question),
|
||||
@@ -319,7 +413,6 @@ private fun showUnsavedSocksHostPortAlert(confirmText: String, save: () -> Unit,
|
||||
fun UseOnionHosts(
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
enabled: State<Boolean>,
|
||||
showModal: (@Composable ModalData.() -> Unit) -> Unit,
|
||||
useOnion: (OnionHosts) -> Unit,
|
||||
) {
|
||||
val values = remember {
|
||||
@@ -331,36 +424,29 @@ fun UseOnionHosts(
|
||||
}
|
||||
}
|
||||
}
|
||||
val onSelected = {
|
||||
showModal {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.network_use_onion_hosts))
|
||||
SectionViewSelectable(null, onionHosts, values, useOnion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (enabled.value) {
|
||||
SectionItemWithValue(
|
||||
generalGetString(MR.strings.network_use_onion_hosts),
|
||||
onionHosts,
|
||||
values,
|
||||
icon = painterResource(MR.images.ic_security),
|
||||
enabled = enabled,
|
||||
onSelected = onSelected
|
||||
)
|
||||
} else {
|
||||
// In reality, when socks proxy is disabled, this option acts like NEVER regardless of what was chosen before
|
||||
SectionItemWithValue(
|
||||
generalGetString(MR.strings.network_use_onion_hosts),
|
||||
remember { mutableStateOf(OnionHosts.NEVER) },
|
||||
listOf(ValueTitleDesc(OnionHosts.NEVER, generalGetString(MR.strings.network_use_onion_hosts_no), AnnotatedString(generalGetString(MR.strings.network_use_onion_hosts_no_desc)))),
|
||||
icon = painterResource(MR.images.ic_security),
|
||||
enabled = enabled,
|
||||
onSelected = {}
|
||||
)
|
||||
Column {
|
||||
if (enabled.value) {
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(MR.strings.network_use_onion_hosts),
|
||||
values.map { it.value to it.title },
|
||||
onionHosts,
|
||||
icon = painterResource(MR.images.ic_security),
|
||||
enabled = enabled,
|
||||
onSelected = useOnion
|
||||
)
|
||||
} else {
|
||||
// In reality, when socks proxy is disabled, this option acts like NEVER regardless of what was chosen before
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(MR.strings.network_use_onion_hosts),
|
||||
listOf(OnionHosts.NEVER to generalGetString(MR.strings.network_use_onion_hosts_no)),
|
||||
remember { mutableStateOf(OnionHosts.NEVER) },
|
||||
icon = painterResource(MR.images.ic_security),
|
||||
enabled = enabled,
|
||||
onSelected = {}
|
||||
)
|
||||
}
|
||||
SectionTextFooter(values.first { it.value == onionHosts.value }.description)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,12 +484,8 @@ fun SessionModePicker(
|
||||
)
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/106223
|
||||
private fun validHost(s: String): Boolean {
|
||||
val validIp = Regex("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])[.]){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$")
|
||||
val validHostname = Regex("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])[.])*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$");
|
||||
return s.matches(validIp) || s.matches(validHostname)
|
||||
}
|
||||
private fun validHost(s: String): Boolean =
|
||||
!s.contains('@')
|
||||
|
||||
// https://ihateregex.io/expr/port/
|
||||
fun validPort(s: String): Boolean {
|
||||
@@ -411,6 +493,16 @@ fun validPort(s: String): Boolean {
|
||||
return s.isNotBlank() && s.matches(validPort)
|
||||
}
|
||||
|
||||
private fun validCredential(s: String): Boolean =
|
||||
!s.contains(':') && !s.contains('@')
|
||||
|
||||
fun showWrongProxyConfigAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.network_proxy_incorrect_config_title),
|
||||
text = generalGetString(MR.strings.network_proxy_incorrect_config_desc),
|
||||
)
|
||||
}
|
||||
|
||||
fun showUpdateNetworkSettingsDialog(
|
||||
title: String,
|
||||
startsWith: String = "",
|
||||
@@ -435,6 +527,7 @@ fun PreviewNetworkAndServersLayout() {
|
||||
NetworkAndServersLayout(
|
||||
currentRemoteHost = null,
|
||||
networkUseSocksProxy = remember { mutableStateOf(true) },
|
||||
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
|
||||
toggleSocksProxy = {},
|
||||
)
|
||||
}
|
||||
|
||||
+16
-20
@@ -1,12 +1,10 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionCustomFooter
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
@@ -34,8 +32,6 @@ import chat.simplex.common.views.onboarding.ReadableText
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.*
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
enum class LAMode {
|
||||
SYSTEM,
|
||||
@@ -374,7 +370,8 @@ fun SimplexLockView(
|
||||
currentLAMode: SharedPreference<LAMode>,
|
||||
setPerformLA: (Boolean) -> Unit
|
||||
) {
|
||||
val performLA = remember { chatModel.performLA }
|
||||
val showAuthScreen = remember { chatModel.showAuthScreen }
|
||||
val performLA = remember { appPrefs.performLA.state }
|
||||
val laMode = remember { chatModel.controller.appPrefs.laMode.state }
|
||||
val laLockDelay = remember { chatModel.controller.appPrefs.laLockDelay }
|
||||
val showChangePasscode = remember { derivedStateOf { performLA.value && currentLAMode.state.value == LAMode.PASSCODE } }
|
||||
@@ -382,13 +379,9 @@ fun SimplexLockView(
|
||||
val selfDestructDisplayName = remember { mutableStateOf(chatModel.controller.appPrefs.selfDestructDisplayName.get() ?: "") }
|
||||
val selfDestructDisplayNamePref = remember { chatModel.controller.appPrefs.selfDestructDisplayName }
|
||||
|
||||
fun resetLAEnabled(onOff: Boolean) {
|
||||
chatModel.controller.appPrefs.performLA.set(onOff)
|
||||
chatModel.performLA.value = onOff
|
||||
}
|
||||
|
||||
fun disableUnavailableLA() {
|
||||
resetLAEnabled(false)
|
||||
chatModel.controller.appPrefs.performLA.set(false)
|
||||
chatModel.showAuthScreen.value = false
|
||||
currentLAMode.set(LAMode.default)
|
||||
laUnavailableInstructionAlert()
|
||||
}
|
||||
@@ -405,7 +398,8 @@ fun SimplexLockView(
|
||||
} else {
|
||||
generalGetString(MR.strings.chat_lock)
|
||||
},
|
||||
generalGetString(MR.strings.change_lock_mode)
|
||||
generalGetString(MR.strings.change_lock_mode),
|
||||
oneTime = true,
|
||||
) { laResult ->
|
||||
when (laResult) {
|
||||
is LAResult.Error -> {
|
||||
@@ -415,7 +409,7 @@ fun SimplexLockView(
|
||||
LAResult.Success -> {
|
||||
when (toLAMode) {
|
||||
LAMode.SYSTEM -> {
|
||||
authenticate(generalGetString(MR.strings.auth_enable_simplex_lock), promptSubtitle = "", usingLAMode = toLAMode) { laResult ->
|
||||
authenticate(generalGetString(MR.strings.auth_enable_simplex_lock), promptSubtitle = "", usingLAMode = toLAMode, oneTime = true) { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
currentLAMode.set(toLAMode)
|
||||
@@ -451,7 +445,7 @@ fun SimplexLockView(
|
||||
}
|
||||
|
||||
fun toggleSelfDestruct(selfDestruct: SharedPreference<Boolean>) {
|
||||
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_mode)) { laResult ->
|
||||
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_mode), oneTime = true) { laResult ->
|
||||
when (laResult) {
|
||||
is LAResult.Error -> laFailedAlert()
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
@@ -470,7 +464,7 @@ fun SimplexLockView(
|
||||
}
|
||||
|
||||
fun changeLAPassword() {
|
||||
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.la_change_app_passcode)) { laResult ->
|
||||
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.la_change_app_passcode), oneTime = true) { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
ModalManager.fullscreen.showCustomModal { close ->
|
||||
@@ -494,7 +488,7 @@ fun SimplexLockView(
|
||||
}
|
||||
|
||||
fun changeSelfDestructPassword() {
|
||||
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_passcode)) { laResult ->
|
||||
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_passcode), oneTime = true) { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
ModalManager.fullscreen.showCustomModal { close ->
|
||||
@@ -525,8 +519,8 @@ fun SimplexLockView(
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.chat_lock))
|
||||
SectionView {
|
||||
EnableLock(performLA) { performLAToggle ->
|
||||
performLA.value = performLAToggle
|
||||
EnableLock(remember { appPrefs.performLA.state }) { performLAToggle ->
|
||||
showAuthScreen.value = performLAToggle
|
||||
chatModel.controller.appPrefs.laNoticeShown.set(true)
|
||||
if (performLAToggle) {
|
||||
when (currentLAMode.state.value) {
|
||||
@@ -543,7 +537,9 @@ fun SimplexLockView(
|
||||
passcodeAlert(generalGetString(MR.strings.passcode_set))
|
||||
},
|
||||
cancel = {
|
||||
resetLAEnabled(false)
|
||||
chatModel.showAuthScreen.value = false
|
||||
// Don't drop auth pref in case of state inconsistency (eg, you have set passcode but somehow bypassed toggle and turned it off and then on)
|
||||
// chatModel.controller.appPrefs.performLA.set(false)
|
||||
},
|
||||
close = close
|
||||
)
|
||||
@@ -660,7 +656,7 @@ private fun EnableSelfDestruct(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EnableLock(performLA: MutableState<Boolean>, onCheckedChange: (Boolean) -> Unit) {
|
||||
private fun EnableLock(performLA: State<Boolean>, onCheckedChange: (Boolean) -> Unit) {
|
||||
SectionItemView {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
|
||||
+3
-1
@@ -22,6 +22,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.CreateProfile
|
||||
@@ -234,7 +235,7 @@ fun ChatLockItem(
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
setPerformLA: (Boolean) -> Unit
|
||||
) {
|
||||
val performLA = remember { ChatModel.performLA }
|
||||
val performLA = remember { appPrefs.performLA.state }
|
||||
val currentLAMode = remember { ChatModel.controller.appPrefs.laMode }
|
||||
SettingsActionItemWithContent(
|
||||
click = showSettingsModal { SimplexLockView(ChatModel, currentLAMode, setPerformLA) },
|
||||
@@ -505,6 +506,7 @@ private fun runAuth(title: String, desc: String, onFinish: (success: Boolean) ->
|
||||
authenticate(
|
||||
title,
|
||||
desc,
|
||||
oneTime = true,
|
||||
completed = { laResult ->
|
||||
onFinish(laResult == LAResult.Success || laResult is LAResult.Unavailable)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -764,7 +765,17 @@
|
||||
<string name="network_socks_proxy">SOCKS proxy</string>
|
||||
<string name="network_socks_proxy_settings">SOCKS proxy settings</string>
|
||||
<string name="network_socks_toggle_use_socks_proxy">Use SOCKS proxy</string>
|
||||
<string name="network_proxy_auth">Proxy authentication</string>
|
||||
<string name="network_proxy_random_credentials">Use random credentials</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_user">Use different proxy credentials for each profile.</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_entity">Use different proxy credentials for each connection.</string>
|
||||
<string name="network_proxy_auth_mode_no_auth">Do not use credentials with proxy.</string>
|
||||
<string name="network_proxy_auth_mode_username_password">Your credentials may be sent unencrypted.</string>
|
||||
<string name="network_proxy_username">Username</string>
|
||||
<string name="network_proxy_password">Password</string>
|
||||
<string name="network_proxy_port">port %d</string>
|
||||
<string name="network_proxy_incorrect_config_title">Error saving proxy</string>
|
||||
<string name="network_proxy_incorrect_config_desc">Make sure proxy configuration is correct.</string>
|
||||
<string name="host_verb">Host</string>
|
||||
<string name="port_verb">Port</string>
|
||||
<string name="network_enable_socks">Use SOCKS proxy?</string>
|
||||
@@ -1556,8 +1567,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 +1904,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 +2185,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":
|
||||
|
||||
@@ -14,7 +14,6 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH
|
||||
import chat.simplex.common.ui.theme.SimpleXTheme
|
||||
@@ -27,7 +26,6 @@ import kotlinx.coroutines.*
|
||||
import java.awt.event.WindowEvent
|
||||
import java.awt.event.WindowFocusListener
|
||||
import java.io.File
|
||||
import kotlin.math.sqrt
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
val simplexWindowState = SimplexWindowState()
|
||||
@@ -172,7 +170,7 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState<Boolean>) {
|
||||
var windowFocused by remember { simplexWindowState.windowFocused }
|
||||
LaunchedEffect(windowFocused) {
|
||||
val delay = ChatController.appPrefs.laLockDelay.get()
|
||||
if (!windowFocused && ChatModel.performLA.value && delay > 0) {
|
||||
if (!windowFocused && ChatModel.showAuthScreen.value && delay > 0) {
|
||||
delay(delay * 1000L)
|
||||
// Trigger auth state check when delay ends (and if it ends)
|
||||
AppLock.recheckAuthState()
|
||||
|
||||
+85
-13
@@ -19,12 +19,76 @@ import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import kotlin.math.min
|
||||
|
||||
data class SemVer(
|
||||
val major: Int,
|
||||
val minor: Int,
|
||||
val patch: Int,
|
||||
val preRelease: String? = null,
|
||||
val buildNumber: Int? = null,
|
||||
): Comparable<SemVer?> {
|
||||
|
||||
val isNotStable: Boolean = preRelease != null
|
||||
|
||||
override fun compareTo(other: SemVer?): Int {
|
||||
if (other == null) return 1
|
||||
return when {
|
||||
major != other.major -> major.compareTo(other.major)
|
||||
minor != other.minor -> minor.compareTo(other.minor)
|
||||
patch != other.patch -> patch.compareTo(other.patch)
|
||||
preRelease != null && other.preRelease != null -> {
|
||||
val pr = preRelease.compareTo(other.preRelease, ignoreCase = true)
|
||||
when {
|
||||
pr != 0 -> pr
|
||||
buildNumber != null && other.buildNumber != null -> buildNumber.compareTo(other.buildNumber)
|
||||
buildNumber != null -> -1
|
||||
other.buildNumber != null -> 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
preRelease != null -> -1
|
||||
other.preRelease != null -> 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val regex = Regex("^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([A-Za-z]+)\\.(\\d+))?\$")
|
||||
fun from(tagName: String): SemVer? {
|
||||
val trimmed = tagName.trimStart { it == 'v' }
|
||||
val redacted = when {
|
||||
trimmed.contains('-') && trimmed.substringBefore('-').count { it == '.' } == 1 -> "${trimmed.substringBefore('-')}.0-${trimmed.substringAfter('-')}"
|
||||
trimmed.substringBefore('-').count { it == '.' } == 1 -> "${trimmed}.0"
|
||||
else -> trimmed
|
||||
}
|
||||
val group = regex.matchEntire(redacted)?.groups
|
||||
return if (group != null) {
|
||||
SemVer(
|
||||
major = group[1]?.value?.toIntOrNull() ?: return null,
|
||||
minor = group[2]?.value?.toIntOrNull() ?: return null,
|
||||
patch = group[3]?.value?.toIntOrNull() ?: return null,
|
||||
preRelease = group[4]?.value,
|
||||
buildNumber = group[5]?.value?.toIntOrNull(),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun fromCurrentVersionName(): SemVer? {
|
||||
val currentVersionName = if (appPlatform.isAndroid) BuildConfigCommon.ANDROID_VERSION_NAME else BuildConfigCommon.DESKTOP_VERSION_NAME
|
||||
return from(currentVersionName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GitHubRelease(
|
||||
@@ -34,12 +98,18 @@ data class GitHubRelease(
|
||||
val htmlUrl: String,
|
||||
val name: String,
|
||||
val draft: Boolean,
|
||||
val prerelease: Boolean,
|
||||
@SerialName("prerelease")
|
||||
private val preRelease: Boolean,
|
||||
val body: String,
|
||||
@SerialName("published_at")
|
||||
val publishedAt: String,
|
||||
val assets: List<GitHubAsset>
|
||||
)
|
||||
) {
|
||||
@Transient
|
||||
val semVer: SemVer? = SemVer.from(tagName)
|
||||
|
||||
val isConsideredBeta: Boolean = preRelease || semVer == null || semVer.isNotStable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GitHubAsset(
|
||||
@@ -105,25 +175,25 @@ private fun createUpdateJob() {
|
||||
|
||||
fun checkForUpdate() {
|
||||
Log.d(TAG, "Checking for update")
|
||||
val currentSemVer = SemVer.fromCurrentVersionName()
|
||||
if (currentSemVer == null) {
|
||||
Log.e(TAG, "Current SemVer cannot be parsed")
|
||||
return
|
||||
}
|
||||
val client = setupHttpClient()
|
||||
try {
|
||||
val request = Request.Builder().url("https://api.github.com/repos/simplex-chat/simplex-chat/releases").addHeader("User-agent", "curl").build()
|
||||
client.newCall(request).execute().use { response ->
|
||||
response.body?.use {
|
||||
val body = it.string()
|
||||
val releases = json.decodeFromString<List<GitHubRelease>>(body).filterNot { it.draft }
|
||||
val releases = json.decodeFromString<List<GitHubRelease>>(body)
|
||||
val release = when (appPrefs.appUpdateChannel.get()) {
|
||||
AppUpdatesChannel.STABLE -> releases.firstOrNull { !it.prerelease }
|
||||
AppUpdatesChannel.BETA -> releases.firstOrNull()
|
||||
AppUpdatesChannel.STABLE -> releases.firstOrNull { r -> !r.draft && !r.isConsideredBeta && currentSemVer < r.semVer }
|
||||
AppUpdatesChannel.BETA -> releases.firstOrNull { r -> !r.draft && currentSemVer < r.semVer }
|
||||
AppUpdatesChannel.DISABLED -> return
|
||||
} ?: return
|
||||
val currentVersionName = "v" + (if (appPlatform.isAndroid) BuildConfigCommon.ANDROID_VERSION_NAME else BuildConfigCommon.DESKTOP_VERSION_NAME)
|
||||
val redactedCurrentVersionName = when {
|
||||
currentVersionName.contains('-') && currentVersionName.substringBefore('-').count { it == '.' } == 1 -> "${currentVersionName.substringBefore('-')}.0-${currentVersionName.substringAfter('-')}"
|
||||
currentVersionName.substringBefore('-').count { it == '.' } == 1 -> "${currentVersionName}.0"
|
||||
else -> currentVersionName
|
||||
}
|
||||
if (release.tagName == appPrefs.appSkippedUpdate.get() || release.tagName == currentVersionName || release.tagName == redactedCurrentVersionName) {
|
||||
|
||||
if (release == null || release.tagName == appPrefs.appSkippedUpdate.get()) {
|
||||
Log.d(TAG, "Skipping update because of the same version or skipped version")
|
||||
return
|
||||
}
|
||||
@@ -298,13 +368,15 @@ private suspend fun downloadAsset(asset: GitHubAsset) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun isRunningFromAppImage(): Boolean = System.getenv("APPIMAGE") != null
|
||||
|
||||
private fun isRunningFromFlatpak(): Boolean = System.getenv("container") == "flatpak"
|
||||
|
||||
private fun chooseGitHubReleaseAssets(release: GitHubRelease): List<GitHubAsset> {
|
||||
val res = if (isRunningFromFlatpak()) {
|
||||
// No need to show download options for Flatpak users
|
||||
emptyList()
|
||||
} else if (Runtime.getRuntime().exec("which dpkg").onExit().join().exitValue() == 0) {
|
||||
} else if (!isRunningFromAppImage() && Runtime.getRuntime().exec("which dpkg").onExit().join().exitValue() == 0) {
|
||||
// Show all available .deb packages and user will choose the one that works on his system (for Debian derivatives)
|
||||
release.assets.filter { it.name.lowercase().endsWith(".deb") }
|
||||
} else {
|
||||
|
||||
+2
-1
@@ -7,10 +7,11 @@ actual fun authenticate(
|
||||
promptSubtitle: String,
|
||||
selfDestruct: Boolean,
|
||||
usingLAMode: LAMode,
|
||||
oneTime: Boolean,
|
||||
completed: (LAResult) -> Unit
|
||||
) {
|
||||
when (usingLAMode) {
|
||||
LAMode.PASSCODE -> authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, completed)
|
||||
LAMode.PASSCODE -> authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, oneTime, completed)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import chat.simplex.common.views.helpers.SemVer
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
// use this command for testing:
|
||||
// ./gradlew desktopTest
|
||||
class SemVerTest {
|
||||
@Test
|
||||
fun testValidSemVer() {
|
||||
assertEquals(SemVer.from("1.0.0"), SemVer(1, 0, 0))
|
||||
assertEquals(SemVer.from("1.0"), SemVer(1, 0, 0))
|
||||
assertEquals(SemVer.from("v1.0"), SemVer(1, 0, 0))
|
||||
assertEquals(SemVer.from("v1.0-beta.1"), SemVer(1, 0, 0, "beta", 1))
|
||||
val r = listOf<Pair<String, SemVer>>(
|
||||
"0.0.4" to SemVer(0, 0, 4),
|
||||
"1.2.3" to SemVer(1, 2, 3),
|
||||
"10.20.30" to SemVer(10, 20, 30),
|
||||
"1.0.0-alpha.1" to SemVer(1, 0, 0, "alpha", buildNumber = 1),
|
||||
"1.0.0" to SemVer(1, 0, 0),
|
||||
"2.0.0" to SemVer(2, 0, 0),
|
||||
"1.1.7" to SemVer(1, 1, 7),
|
||||
"2.0.1-alpha.1227" to SemVer(2, 0, 1, "alpha", 1227),
|
||||
)
|
||||
r.forEach { (value, correct) ->
|
||||
assertEquals(SemVer.from(value), correct)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testComparisonSemVer() {
|
||||
assert(SemVer(0, 1, 0) == SemVer.from("0.1.0"))
|
||||
assert(SemVer(1, 1, 0) == SemVer.from("v1.1.0"))
|
||||
assert(SemVer(0, 1, 0) > SemVer(0, 0, 1))
|
||||
assert(SemVer(1, 0, 0) > SemVer(0, 100, 100))
|
||||
assert(SemVer(0, 200, 0) > SemVer(0, 100, 100))
|
||||
assert(SemVer(0, 1, 0, "beta") > SemVer(0, 1, 0, "alpha"))
|
||||
assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "alpha"))
|
||||
assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "beta"))
|
||||
assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "beta.0"))
|
||||
assert(SemVer(0, 1, 0, "beta", 1) > SemVer(0, 1, 0, "beta", 0))
|
||||
assert(SemVer(0, 1, 0, "beta", 11) > SemVer(0, 1, 0, "beta", 10))
|
||||
assert(SemVer(0, 1, 0, "beta", 11) > SemVer(0, 1, 0, "beta", 9))
|
||||
assert(SemVer(0, 1, 0, "beta.1") > SemVer(0, 1, 0, "alpha.2"))
|
||||
assert(SemVer(1, 1, 0, "beta.1") > SemVer(0, 1, 0, "beta.1"))
|
||||
assert(SemVer(1, 0, 0) > SemVer(1, 0, 0, "beta.1"))
|
||||
assert(SemVer(1, 0, 0) > null)
|
||||
assert(SemVer.from("v6.0.0")!! > SemVer.from("v6.0.0-beta.3"))
|
||||
assert(SemVer.from("v6.0.0-beta.3")!! > SemVer.from("v6.0.0-beta.2"))
|
||||
assert(SemVer.from("0.1.0") == SemVer.from("0.1.0"))
|
||||
assert(SemVer.from("0.1.1")!! > SemVer.from("0.1.0"))
|
||||
assert(SemVer.from("0.2.1")!! > SemVer.from("0.1.1"))
|
||||
assert(SemVer.from("2.0.1")!! > SemVer.from("0.1.1"))
|
||||
assert(SemVer.from("0.1.1-beta.0")!! > SemVer.from("0.1.0-beta.0"))
|
||||
assert(SemVer.from("0.1.1-beta.0")!! == SemVer.from("0.1.1-beta.0"))
|
||||
assert(SemVer.from("0.1.1-beta.1")!! > SemVer.from("0.1.1-beta.0"))
|
||||
assert(SemVer.from("10.0.0-beta.12")!! > SemVer.from("1.1.1"))
|
||||
assert(SemVer.from("1.1.1-beta.120")!! > SemVer.from("1.1.1-alpha.9"))
|
||||
assert(SemVer.from("1.1.1-beta.120")!! > SemVer.from("1.1.1-alpha.120"))
|
||||
assert(SemVer.from("2.0.1")!! > SemVer.from("0.1.1"))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -26,11 +26,11 @@ android.enableJetifier=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
kotlin.jvm.target=11
|
||||
|
||||
android.version_name=6.0.3
|
||||
android.version_code=235
|
||||
android.version_name=6.0.5
|
||||
android.version_code=241
|
||||
|
||||
desktop.version_name=6.0.3
|
||||
desktop.version_code=64
|
||||
desktop.version_name=6.0.5
|
||||
desktop.version_code=68
|
||||
|
||||
kotlin.version=1.9.23
|
||||
gradle.plugin.version=8.2.0
|
||||
|
||||
@@ -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
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 56986f82c89b04beae84a61208db8b55eb0098e3
|
||||
tag: 4268b90763c58358809a3ea7dd8bc7d78eeb3077
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user