mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e6831d46f | |||
| 2f730d54e9 | |||
| fe0013c4a9 | |||
| 5261886b31 | |||
| 93ab3076d4 | |||
| 3b88ddbd4f | |||
| d6dc35738e | |||
| 5b3aba9db2 | |||
| 981cbb8bf9 | |||
| fc3ae49214 | |||
| 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 | |||
| 0118e64ab4 | |||
| 4552860345 | |||
| 4d18174b11 | |||
| defd095a4f | |||
| ed60f28e56 | |||
| f0b889ffcf | |||
| a95415fa1a | |||
| 8d48c4b14c | |||
| 9f44242e4c | |||
| 7e7d93c596 | |||
| 073818db55 | |||
| 519dd9e219 | |||
| 94218a1a7e | |||
| 996c6efddd | |||
| fd9c080103 | |||
| 5cb8badb22 | |||
| 0438f35539 | |||
| d5eb7b7811 | |||
| e04f74738e | |||
| 5f0ccb9f17 | |||
| b2d18f6960 | |||
| b52dfee078 | |||
| 70991debfd | |||
| 885aa9cfa5 | |||
| 75a468434c | |||
| 3b98032371 |
@@ -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)")
|
||||
|
||||
@@ -36,7 +36,7 @@ struct ContentView: View {
|
||||
@State private var waitingForOrPassedAuth = true
|
||||
@State private var chatListActionSheet: ChatListActionSheet? = nil
|
||||
|
||||
private let callTopPadding: CGFloat = 50
|
||||
private let callTopPadding: CGFloat = 40
|
||||
|
||||
private enum ChatListActionSheet: Identifiable {
|
||||
case planAndConnectSheet(sheet: PlanAndConnectActionSheet)
|
||||
@@ -151,12 +151,12 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
reactOnDarkThemeChanges()
|
||||
reactOnDarkThemeChanges(systemInDarkThemeCurrently)
|
||||
}
|
||||
.onChange(of: colorScheme) { scheme in
|
||||
// It's needed to update UI colors when iOS wants to make screenshot after going to background,
|
||||
// so when a user changes his global theme from dark to light or back, the app will adapt to it
|
||||
reactOnDarkThemeChanges()
|
||||
reactOnDarkThemeChanges(scheme == .dark)
|
||||
}
|
||||
.onChange(of: theme.name) { _ in
|
||||
ThemeManager.adjustWindowStyle()
|
||||
@@ -207,7 +207,7 @@ struct ContentView: View {
|
||||
CallDuration(call: call)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.frame(height: callTopPadding - 10)
|
||||
.frame(height: callTopPadding)
|
||||
.background(Color(uiColor: UIColor(red: 47/255, green: 208/255, blue: 88/255, alpha: 1)))
|
||||
.onTapGesture {
|
||||
chatModel.activeCallViewIsCollapsed = false
|
||||
|
||||
@@ -143,7 +143,7 @@ final class ChatModel: ObservableObject {
|
||||
@Published var contentViewAccessAuthenticated: Bool = false
|
||||
@Published var laRequest: LocalAuthRequest?
|
||||
// list of chat "previews"
|
||||
@Published var chats: [Chat] = []
|
||||
@Published private(set) var chats: [Chat] = []
|
||||
@Published var deletedChats: Set<String> = []
|
||||
// current chat
|
||||
@Published var chatId: String?
|
||||
@@ -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> = [:]
|
||||
@@ -357,25 +356,8 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func updateChats(with newChats: [ChatData]) {
|
||||
for i in 0..<newChats.count {
|
||||
let c = newChats[i]
|
||||
if let j = getChatIndex(c.id) {
|
||||
let chat = chats[j]
|
||||
chat.chatInfo = c.chatInfo
|
||||
chat.chatItems = c.chatItems
|
||||
chat.chatStats = c.chatStats
|
||||
if i != j {
|
||||
if chatId != c.chatInfo.id {
|
||||
popChat_(j, to: i)
|
||||
} else if i == 0 {
|
||||
chatToTop = c.chatInfo.id
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addChat_(Chat(c), at: i)
|
||||
}
|
||||
}
|
||||
func updateChats(_ newChats: [ChatData]) {
|
||||
chats = newChats.map { Chat($0) }
|
||||
NtfManager.shared.setNtfBadgeCount(totalUnreadCountForAllUsers())
|
||||
popChatCollector.clear()
|
||||
}
|
||||
|
||||
@@ -112,9 +112,9 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? =
|
||||
return resp
|
||||
}
|
||||
|
||||
func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, _ ctrl: chat_ctrl? = nil) async -> ChatResponse {
|
||||
func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, _ ctrl: chat_ctrl? = nil, log: Bool = true) async -> ChatResponse {
|
||||
await withCheckedContinuation { cont in
|
||||
cont.resume(returning: chatSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl))
|
||||
cont.resume(returning: chatSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl, log: log))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -582,13 +582,13 @@ func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (Gro
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, QueueInfo) {
|
||||
func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo) {
|
||||
let r = await chatSendCmd(.apiContactQueueInfo(contactId: contactId))
|
||||
if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, QueueInfo) {
|
||||
func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo) {
|
||||
let r = await chatSendCmd(.apiGroupMemberQueueInfo(groupId: groupId, groupMemberId: groupMemberId))
|
||||
if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) }
|
||||
throw r
|
||||
@@ -1218,12 +1218,18 @@ func apiEndCall(_ contact: Contact) async throws {
|
||||
try await sendCommandOkResp(.apiEndCall(contact: contact))
|
||||
}
|
||||
|
||||
func apiGetCallInvitations() throws -> [RcvCallInvitation] {
|
||||
func apiGetCallInvitationsSync() throws -> [RcvCallInvitation] {
|
||||
let r = chatSendCmdSync(.apiGetCallInvitations)
|
||||
if case let .callInvitations(invs) = r { return invs }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetCallInvitations() async throws -> [RcvCallInvitation] {
|
||||
let r = await chatSendCmd(.apiGetCallInvitations)
|
||||
if case let .callInvitations(invs) = r { return invs }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiCallStatus(_ contact: Contact, _ status: String) async throws {
|
||||
if let callStatus = WebRTCCallStatus.init(rawValue: status) {
|
||||
try await sendCommandOkResp(.apiCallStatus(contact: contact, callStatus: callStatus))
|
||||
@@ -1422,7 +1428,7 @@ func apiGetVersion() throws -> CoreVersionInfo {
|
||||
|
||||
func getAgentSubsTotal() async throws -> (SMPServerSubs, Bool) {
|
||||
let userId = try currentUserId("getAgentSubsTotal")
|
||||
let r = await chatSendCmd(.getAgentSubsTotal(userId: userId))
|
||||
let r = await chatSendCmd(.getAgentSubsTotal(userId: userId), log: false)
|
||||
if case let .agentSubsTotal(_, subsTotal, hasSession) = r { return (subsTotal, hasSession) }
|
||||
logger.error("getAgentSubsTotal error: \(String(describing: r))")
|
||||
throw r
|
||||
@@ -1517,7 +1523,7 @@ func startChat(refreshInvitations: Bool = true) throws {
|
||||
try getUserChatData()
|
||||
NtfManager.shared.setNtfBadgeCount(m.totalUnreadCountForAllUsers())
|
||||
if (refreshInvitations) {
|
||||
try refreshCallInvitations()
|
||||
Task { try await refreshCallInvitations() }
|
||||
}
|
||||
(m.savedToken, m.tokenStatus, m.notificationMode, m.notificationServer) = apiGetNtfToken()
|
||||
_ = try apiStartChat()
|
||||
@@ -1591,8 +1597,7 @@ func getUserChatData() throws {
|
||||
m.userAddress = try apiGetUserAddress()
|
||||
m.chatItemTTL = try getChatItemTTL()
|
||||
let chats = try apiGetChats()
|
||||
m.chats = chats.map { Chat.init($0) }
|
||||
m.popChatCollector.clear()
|
||||
m.updateChats(chats)
|
||||
}
|
||||
|
||||
private func getUserChatDataAsync() async throws {
|
||||
@@ -1604,14 +1609,12 @@ private func getUserChatDataAsync() async throws {
|
||||
await MainActor.run {
|
||||
m.userAddress = userAddress
|
||||
m.chatItemTTL = chatItemTTL
|
||||
m.chats = chats.map { Chat.init($0) }
|
||||
m.popChatCollector.clear()
|
||||
m.updateChats(chats)
|
||||
}
|
||||
} else {
|
||||
await MainActor.run {
|
||||
m.userAddress = nil
|
||||
m.chats = []
|
||||
m.popChatCollector.clear()
|
||||
m.updateChats([])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2164,32 +2167,42 @@ func chatItemSimpleUpdate(_ user: any UserLike, _ aChatItem: AChatItem) async {
|
||||
}
|
||||
}
|
||||
|
||||
func refreshCallInvitations() throws {
|
||||
func refreshCallInvitations() async throws {
|
||||
let m = ChatModel.shared
|
||||
let callInvitations = try justRefreshCallInvitations()
|
||||
if let (chatId, ntfAction) = m.ntfCallInvitationAction,
|
||||
let invitation = m.callInvitations.removeValue(forKey: chatId) {
|
||||
m.ntfCallInvitationAction = nil
|
||||
CallController.shared.callAction(invitation: invitation, action: ntfAction)
|
||||
} else if let invitation = callInvitations.last(where: { $0.user.showNotifications }) {
|
||||
activateCall(invitation)
|
||||
let callInvitations = try await apiGetCallInvitations()
|
||||
await MainActor.run {
|
||||
m.callInvitations = callsByChat(callInvitations)
|
||||
if let (chatId, ntfAction) = m.ntfCallInvitationAction,
|
||||
let invitation = m.callInvitations.removeValue(forKey: chatId) {
|
||||
m.ntfCallInvitationAction = nil
|
||||
CallController.shared.callAction(invitation: invitation, action: ntfAction)
|
||||
} else if let invitation = callInvitations.last(where: { $0.user.showNotifications }) {
|
||||
activateCall(invitation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func justRefreshCallInvitations() throws -> [RcvCallInvitation] {
|
||||
let m = ChatModel.shared
|
||||
let callInvitations = try apiGetCallInvitations()
|
||||
m.callInvitations = callInvitations.reduce(into: [ChatId: RcvCallInvitation]()) { result, inv in result[inv.contact.id] = inv }
|
||||
return callInvitations
|
||||
func justRefreshCallInvitations() async throws {
|
||||
let callInvitations = try apiGetCallInvitationsSync()
|
||||
await MainActor.run {
|
||||
ChatModel.shared.callInvitations = callsByChat(callInvitations)
|
||||
}
|
||||
}
|
||||
|
||||
private func callsByChat(_ callInvitations: [RcvCallInvitation]) -> [ChatId: RcvCallInvitation] {
|
||||
callInvitations.reduce(into: [ChatId: RcvCallInvitation]()) {
|
||||
result, inv in result[inv.contact.id] = inv
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -83,9 +83,11 @@ struct SimpleXApp: App {
|
||||
if appState != .stopped {
|
||||
startChatAndActivate {
|
||||
if appState.inactive && chatModel.chatRunning == true {
|
||||
updateChats()
|
||||
if !chatModel.showCallView && !CallController.shared.hasActiveCalls() {
|
||||
updateCallInvitations()
|
||||
Task {
|
||||
await updateChats()
|
||||
if !chatModel.showCallView && !CallController.shared.hasActiveCalls() {
|
||||
await updateCallInvitations()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,16 +132,16 @@ struct SimpleXApp: App {
|
||||
}
|
||||
}
|
||||
|
||||
private func updateChats() {
|
||||
private func updateChats() async {
|
||||
do {
|
||||
let chats = try apiGetChats()
|
||||
chatModel.updateChats(with: chats)
|
||||
let chats = try await apiGetChatsAsync()
|
||||
await MainActor.run { chatModel.updateChats(chats) }
|
||||
if let id = chatModel.chatId,
|
||||
let chat = chatModel.getChat(id) {
|
||||
Task { await loadChat(chat: chat, clearItems: false) }
|
||||
}
|
||||
if let ncr = chatModel.ntfContactRequest {
|
||||
chatModel.ntfContactRequest = nil
|
||||
await MainActor.run { chatModel.ntfContactRequest = nil }
|
||||
if case let .contactRequest(contactRequest) = chatModel.getChat(ncr.chatId)?.chatInfo {
|
||||
Task { await acceptContactRequest(incognito: ncr.incognito, contactRequest: contactRequest) }
|
||||
}
|
||||
@@ -149,9 +151,9 @@ struct SimpleXApp: App {
|
||||
}
|
||||
}
|
||||
|
||||
private func updateCallInvitations() {
|
||||
private func updateCallInvitations() async {
|
||||
do {
|
||||
try refreshCallInvitations()
|
||||
try await refreshCallInvitations()
|
||||
} catch let error {
|
||||
logger.error("apiGetCallInvitations: cannot update call invitations \(responseError(error))")
|
||||
}
|
||||
|
||||
@@ -91,8 +91,8 @@ var systemInDarkThemeCurrently: Bool {
|
||||
return UITraitCollection.current.userInterfaceStyle == .dark
|
||||
}
|
||||
|
||||
func reactOnDarkThemeChanges() {
|
||||
if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME && CurrentColors.colors.isLight == systemInDarkThemeCurrently {
|
||||
func reactOnDarkThemeChanges(_ inDarkNow: Bool) {
|
||||
if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME && CurrentColors.colors.isLight == inDarkNow {
|
||||
// Change active colors from light to dark and back based on system theme
|
||||
ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ class ThemeManager {
|
||||
return perUserTheme
|
||||
}
|
||||
let defaultTheme = defaultActiveTheme(appSettingsTheme)
|
||||
return ThemeModeOverride(mode: CurrentColors.base.mode, colors: defaultTheme?.colors ?? ThemeColors(), wallpaper: defaultTheme?.wallpaper)
|
||||
return ThemeModeOverride(mode: CurrentColors.base.mode, colors: defaultTheme?.colors ?? ThemeColors(), wallpaper: defaultTheme?.wallpaper ?? ThemeWallpaper.from(PresetWallpaper.school.toType(CurrentColors.base), nil, nil))
|
||||
}
|
||||
|
||||
static func currentColors(_ themeOverridesForType: WallpaperType?, _ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverrides?, _ appSettingsTheme: [ThemeOverrides]) -> ActiveTheme {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -411,6 +411,15 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
||||
}
|
||||
|
||||
func endCall() {
|
||||
if #available(iOS 16.0, *) {
|
||||
_endCall()
|
||||
} else {
|
||||
// Fixes `connection.close()` getting locked up in iOS15
|
||||
DispatchQueue.global(qos: .utility).async { self._endCall() }
|
||||
}
|
||||
}
|
||||
|
||||
private func _endCall() {
|
||||
guard let call = activeCall.wrappedValue else { return }
|
||||
logger.debug("WebRTCClient: ending the call")
|
||||
activeCall.wrappedValue = nil
|
||||
|
||||
@@ -671,7 +671,14 @@ private struct CallButton: View {
|
||||
|
||||
InfoViewButton(image: image, title: title, disabledLook: !canCall, width: width) {
|
||||
if canCall {
|
||||
CallController.shared.startCall(contact, mediaType)
|
||||
if CallController.useCallKit() {
|
||||
CallController.shared.startCall(contact, mediaType)
|
||||
} else {
|
||||
// When CallKit is not used, colorscheme will be changed and it will be visible if not hiding sheets first
|
||||
dismissAllSheets(animated: true) {
|
||||
CallController.shared.startCall(contact, mediaType)
|
||||
}
|
||||
}
|
||||
} else if contact.nextSendGrpInv {
|
||||
showAlert(SomeAlert(
|
||||
alert: mkAlert(
|
||||
@@ -935,7 +942,7 @@ func syncConnectionForceAlert(_ syncConnectionForce: @escaping () -> Void) -> Al
|
||||
)
|
||||
}
|
||||
|
||||
func queueInfoText(_ info: (RcvMsgInfo?, QueueInfo)) -> String {
|
||||
func queueInfoText(_ info: (RcvMsgInfo?, ServerQueueInfo)) -> String {
|
||||
let (rcvMsgInfo, qInfo) = info
|
||||
var msgInfo: String
|
||||
if let rcvMsgInfo { msgInfo = encodeJSON(rcvMsgInfo) } else { msgInfo = "none" }
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
@@ -180,6 +180,13 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
|
||||
snapshot,
|
||||
animatingDifferences: itemCount != 0 && abs(items.count - itemCount) == 1
|
||||
)
|
||||
// Sets content offset on initial load
|
||||
if itemCount == 0 {
|
||||
tableView.setContentOffset(
|
||||
CGPoint(x: 0, y: -InvertedTableView.inset),
|
||||
animated: false
|
||||
)
|
||||
}
|
||||
itemCount = items.count
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,14 +217,31 @@ struct ChatListView: View {
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
ForEach(cs, id: \.viewId) { chat in
|
||||
ChatListNavLink(chat: chat)
|
||||
if #available(iOS 16.0, *) {
|
||||
ForEach(cs, id: \.viewId) { chat in
|
||||
ChatListNavLink(chat: chat)
|
||||
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||
.padding(.trailing, -16)
|
||||
.disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id))
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
.offset(x: -8)
|
||||
} else {
|
||||
ForEach(cs, id: \.viewId) { chat in
|
||||
VStack(spacing: .zero) {
|
||||
Divider()
|
||||
.padding(.leading, 16)
|
||||
ChatListNavLink(chat: chat)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||
.padding(.trailing, -16)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(EdgeInsets())
|
||||
.background { theme.colors.background } // Hides default list selection colour
|
||||
.disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id))
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
}
|
||||
.offset(x: -8)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.onChange(of: chatModel.chatId) { currentChatId in
|
||||
@@ -501,21 +518,21 @@ func chatStoppedIcon() -> some View {
|
||||
struct ChatListView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chatModel = ChatModel()
|
||||
chatModel.chats = [
|
||||
Chat(
|
||||
chatModel.updateChats([
|
||||
ChatData(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")]
|
||||
),
|
||||
Chat(
|
||||
ChatData(
|
||||
chatInfo: ChatInfo.sampleData.group,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")]
|
||||
),
|
||||
Chat(
|
||||
ChatData(
|
||||
chatInfo: ChatInfo.sampleData.contactRequest,
|
||||
chatItems: []
|
||||
)
|
||||
|
||||
]
|
||||
])
|
||||
return Group {
|
||||
ChatListView(showSettings: Binding.constant(false))
|
||||
.environmentObject(chatModel)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -85,15 +85,18 @@ struct UserPicker: View {
|
||||
.padding(8)
|
||||
.opacity(userPickerVisible ? 1.0 : 0.0)
|
||||
.onAppear {
|
||||
do {
|
||||
// This check prevents the call of listUsers after the app is suspended, and the database is closed.
|
||||
if case .active = scenePhase {
|
||||
m.users = try listUsers()
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("Error loading users \(responseError(error))")
|
||||
}
|
||||
}
|
||||
// This check prevents the call of listUsers after the app is suspended, and the database is closed.
|
||||
if case .active = scenePhase {
|
||||
Task {
|
||||
do {
|
||||
let users = try await listUsersAsync()
|
||||
await MainActor.run { m.users = users }
|
||||
} catch {
|
||||
logger.error("Error loading users \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func userView(_ u: UserInfo) -> some View {
|
||||
|
||||
@@ -491,7 +491,7 @@ struct DatabaseView: View {
|
||||
appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory())
|
||||
do {
|
||||
let chats = try apiGetChats()
|
||||
m.updateChats(with: chats)
|
||||
m.updateChats(chats)
|
||||
} catch let error {
|
||||
logger.error("apiGetChats: cannot update chats \(responseError(error))")
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ extension PresetWallpaper {
|
||||
scale
|
||||
} else if let type = ChatModel.shared.currentUser?.uiThemes?.preferredMode(base.mode == DefaultThemeMode.dark)?.wallpaper?.toAppWallpaper().type, type.sameType(WallpaperType.preset(filename, nil)) {
|
||||
type.scale
|
||||
} else if let scale = themeOverridesDefault.get().first(where: { $0.wallpaper != nil && $0.wallpaper!.preset == filename })?.wallpaper?.scale {
|
||||
} else if let scale = themeOverridesDefault.get().first(where: { $0.wallpaper != nil && $0.wallpaper!.preset == filename && $0.base == base })?.wallpaper?.scale {
|
||||
scale
|
||||
} else {
|
||||
Float(1.0)
|
||||
|
||||
@@ -65,8 +65,7 @@ struct LocalAuthView: View {
|
||||
// Clear sensitive data on screen just in case app fails to hide its views while new database is created
|
||||
m.chatId = nil
|
||||
ItemsModel.shared.reversedChatItems = []
|
||||
m.chats = []
|
||||
m.popChatCollector.clear()
|
||||
m.updateChats([])
|
||||
m.users = []
|
||||
_ = kcAppPassword.set(password)
|
||||
_ = kcSelfDestructPassword.remove()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -802,7 +802,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to send SimpleX links." xml:space="preserve">
|
||||
<source>Allow to send SimpleX links.</source>
|
||||
<target>Permitir enviar enlaces SimpleX.</target>
|
||||
<target>Se permite enviar enlaces SimpleX.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to send files and media." xml:space="preserve">
|
||||
@@ -2406,7 +2406,7 @@ This is your own one-time link!</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="Do not send history to new members." xml:space="preserve">
|
||||
<source>Do not send history to new members.</source>
|
||||
<target>No enviar historial a miembros nuevos.</target>
|
||||
<target>No se envía el historial a los miembros nuevos.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Don't create address" xml:space="preserve">
|
||||
@@ -5068,7 +5068,7 @@ Error: %@</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Prohibit sending SimpleX links." xml:space="preserve">
|
||||
<source>Prohibit sending SimpleX links.</source>
|
||||
<target>No permitir el envío de enlaces SimpleX.</target>
|
||||
<target>No se permite enviar enlaces SimpleX.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Prohibit sending direct messages to members." xml:space="preserve">
|
||||
@@ -5841,7 +5841,7 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send up to 100 last messages to new members." xml:space="preserve">
|
||||
<source>Send up to 100 last messages to new members.</source>
|
||||
<target>Enviar hasta 100 últimos mensajes a los miembros nuevos.</target>
|
||||
<target>Se envían hasta 100 mensajes más recientes a los miembros nuevos.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
|
||||
@@ -7449,7 +7449,7 @@ Repeat join request?</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can change it in Appearance settings." xml:space="preserve">
|
||||
<source>You can change it in Appearance settings.</source>
|
||||
<target>Puede cambiarlo desde el menú Apariencia.</target>
|
||||
<target>Puedes cambiar la posición de la barra desde el menú Apariencia.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can create it later" xml:space="preserve">
|
||||
|
||||
@@ -1042,7 +1042,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Bad message hash" xml:space="preserve">
|
||||
<source>Bad message hash</source>
|
||||
<target>Téves üzenet hash</target>
|
||||
<target>Hibás az üzenet ellenőrzőösszege</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Better groups" xml:space="preserve">
|
||||
@@ -2131,7 +2131,7 @@ Ez az egyszer használatos hivatkozása!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete member message?" xml:space="preserve">
|
||||
<source>Delete member message?</source>
|
||||
<target>Csoporttag üzenet törlése?</target>
|
||||
<target>Csoporttag üzenetének törlése?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete message?" xml:space="preserve">
|
||||
@@ -2221,7 +2221,7 @@ Ez az egyszer használatos hivatkozása!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delivery receipts!" xml:space="preserve">
|
||||
<source>Delivery receipts!</source>
|
||||
<target>Kézbesítési igazolások!</target>
|
||||
<target>Üzenet kézbesítési jelentések!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Description" xml:space="preserve">
|
||||
@@ -2681,12 +2681,12 @@ Ez az egyszer használatos hivatkozása!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter welcome message…" xml:space="preserve">
|
||||
<source>Enter welcome message…</source>
|
||||
<target>Üdvözlő üzenetet megadása…</target>
|
||||
<target>Üdvözlő üzenet megadása…</target>
|
||||
<note>placeholder</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter welcome message… (optional)" xml:space="preserve">
|
||||
<source>Enter welcome message… (optional)</source>
|
||||
<target>Üdvözlő üzenetet megadása… (opcionális)</target>
|
||||
<target>Üdvözlő üzenet megadása… (opcionális)</target>
|
||||
<note>placeholder</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter your name…" xml:space="preserve">
|
||||
@@ -4131,7 +4131,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message delivery receipts!" xml:space="preserve">
|
||||
<source>Message delivery receipts!</source>
|
||||
<target>Üzenetkézbesítési bizonylatok!</target>
|
||||
<target>Üzenet kézbesítési jelentések!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message delivery warning" xml:space="preserve">
|
||||
@@ -4176,7 +4176,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message reception" xml:space="preserve">
|
||||
<source>Message reception</source>
|
||||
<target>Üzenetjelentés</target>
|
||||
<target>Üzenet kézbesítési jelentés</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message servers" xml:space="preserve">
|
||||
@@ -6573,7 +6573,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="The hash of the previous message is different." xml:space="preserve">
|
||||
<source>The hash of the previous message is different.</source>
|
||||
<target>Az előző üzenet hash-e más.</target>
|
||||
<target>Az előző üzenet ellenőrzőösszege különbözik.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The message will be deleted for all members." xml:space="preserve">
|
||||
@@ -7888,7 +7888,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="bad message hash" xml:space="preserve">
|
||||
<source>bad message hash</source>
|
||||
<target>téves üzenet hash</target>
|
||||
<target>hibás az üzenet ellenőrzőösszege</target>
|
||||
<note>integrity error chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="blocked" xml:space="preserve">
|
||||
@@ -8490,7 +8490,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="removed %@" xml:space="preserve">
|
||||
<source>removed %@</source>
|
||||
<target>%@ eltávolítva</target>
|
||||
<target>eltávolította őt: %@</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="removed contact address" xml:space="preserve">
|
||||
|
||||
@@ -1617,7 +1617,7 @@ Questo è il tuo link una tantum!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connection and servers status." xml:space="preserve">
|
||||
<source>Connection and servers status.</source>
|
||||
<target>Stato di connessione e server.</target>
|
||||
<target>Stato della connessione e dei server.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connection error" xml:space="preserve">
|
||||
|
||||
@@ -3906,7 +3906,7 @@ Dit is jouw link voor groep %@!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Keep conversation" xml:space="preserve">
|
||||
<source>Keep conversation</source>
|
||||
<target>Blijf in gesprek</target>
|
||||
<target>Behoud het gesprek</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Keep the app open to use it from desktop" xml:space="preserve">
|
||||
|
||||
@@ -752,6 +752,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow calls?" xml:space="preserve">
|
||||
<source>Allow calls?</source>
|
||||
<target>Zezwolić na połączenia?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve">
|
||||
@@ -791,6 +792,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sharing" xml:space="preserve">
|
||||
<source>Allow sharing</source>
|
||||
<target>Zezwól na udostępnianie</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve">
|
||||
@@ -945,10 +947,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Archive contacts to chat later." xml:space="preserve">
|
||||
<source>Archive contacts to chat later.</source>
|
||||
<target>Archiwizuj kontakty aby porozmawiać później.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Archived contacts" xml:space="preserve">
|
||||
<source>Archived contacts</source>
|
||||
<target>Zarchiwizowane kontakty</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Archiving database" xml:space="preserve">
|
||||
@@ -1053,6 +1057,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Better networking" xml:space="preserve">
|
||||
<source>Better networking</source>
|
||||
<target>Lepsze sieciowanie</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Black" xml:space="preserve">
|
||||
@@ -1097,10 +1102,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Blur for better privacy." xml:space="preserve">
|
||||
<source>Blur for better privacy.</source>
|
||||
<target>Rozmycie dla lepszej prywatności.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Blur media" xml:space="preserve">
|
||||
<source>Blur media</source>
|
||||
<target>Rozmycie mediów</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
|
||||
@@ -1150,6 +1157,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Calls prohibited!" xml:space="preserve">
|
||||
<source>Calls prohibited!</source>
|
||||
<target>Połączenia zakazane!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Camera not available" xml:space="preserve">
|
||||
@@ -1159,10 +1167,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't call contact" xml:space="preserve">
|
||||
<source>Can't call contact</source>
|
||||
<target>Nie można zadzwonić do kontaktu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't call member" xml:space="preserve">
|
||||
<source>Can't call member</source>
|
||||
<target>Nie można zadzwonić do członka</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't invite contact!" xml:space="preserve">
|
||||
@@ -1177,6 +1187,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't message member" xml:space="preserve">
|
||||
<source>Can't message member</source>
|
||||
<target>Nie można wysłać wiadomości do członka</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
@@ -1292,6 +1303,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat database exported" xml:space="preserve">
|
||||
<source>Chat database exported</source>
|
||||
<target>Wyeksportowano bazę danych czatu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat database imported" xml:space="preserve">
|
||||
@@ -1316,6 +1328,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat list" xml:space="preserve">
|
||||
<source>Chat list</source>
|
||||
<target>Lista czatów</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat migrated!" xml:space="preserve">
|
||||
@@ -1405,6 +1418,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Color chats with the new themes." xml:space="preserve">
|
||||
<source>Color chats with the new themes.</source>
|
||||
<target>Koloruj czaty z nowymi motywami.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Color mode" xml:space="preserve">
|
||||
@@ -1449,6 +1463,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm contact deletion?" xml:space="preserve">
|
||||
<source>Confirm contact deletion?</source>
|
||||
<target>Potwierdzić usunięcie kontaktu?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm database upgrades" xml:space="preserve">
|
||||
@@ -1508,6 +1523,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect to your friends faster." xml:space="preserve">
|
||||
<source>Connect to your friends faster.</source>
|
||||
<target>Szybciej łącz się ze znajomymi.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect to yourself?" xml:space="preserve">
|
||||
@@ -1586,6 +1602,7 @@ To jest twój jednorazowy link!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connecting to contact, please wait or check later!" xml:space="preserve">
|
||||
<source>Connecting to contact, please wait or check later!</source>
|
||||
<target>Łączenie z kontaktem, poczekaj lub sprawdź później!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connecting to desktop" xml:space="preserve">
|
||||
@@ -1600,6 +1617,7 @@ To jest twój jednorazowy link!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connection and servers status." xml:space="preserve">
|
||||
<source>Connection and servers status.</source>
|
||||
<target>Stan połączenia i serwerów.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connection error" xml:space="preserve">
|
||||
@@ -1614,6 +1632,7 @@ To jest twój jednorazowy link!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connection notifications" xml:space="preserve">
|
||||
<source>Connection notifications</source>
|
||||
<target>Powiadomienia o połączeniu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connection request sent!" xml:space="preserve">
|
||||
@@ -1653,6 +1672,7 @@ To jest twój jednorazowy link!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact deleted!" xml:space="preserve">
|
||||
<source>Contact deleted!</source>
|
||||
<target>Kontakt usunięty!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact hidden:" xml:space="preserve">
|
||||
@@ -1667,6 +1687,7 @@ To jest twój jednorazowy link!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact is deleted." xml:space="preserve">
|
||||
<source>Contact is deleted.</source>
|
||||
<target>Kontakt jest usunięty.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact name" xml:space="preserve">
|
||||
@@ -1681,6 +1702,7 @@ To jest twój jednorazowy link!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact will be deleted - this cannot be undone!" xml:space="preserve">
|
||||
<source>Contact will be deleted - this cannot be undone!</source>
|
||||
<target>Kontakt zostanie usunięty – nie można tego cofnąć!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contacts" xml:space="preserve">
|
||||
@@ -1700,6 +1722,7 @@ To jest twój jednorazowy link!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Conversation deleted!" xml:space="preserve">
|
||||
<source>Conversation deleted!</source>
|
||||
<target>Rozmowa usunięta!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Copy" xml:space="preserve">
|
||||
@@ -1978,6 +2001,7 @@ To jest twój jednorazowy link!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages of members?" xml:space="preserve">
|
||||
<source>Delete %lld messages of members?</source>
|
||||
<target>Usunąć %lld wiadomości członków?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete %lld messages?" xml:space="preserve">
|
||||
@@ -2042,6 +2066,7 @@ To jest twój jednorazowy link!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete contact?" xml:space="preserve">
|
||||
<source>Delete contact?</source>
|
||||
<target>Usunąć kontakt?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete database" xml:space="preserve">
|
||||
@@ -2151,6 +2176,7 @@ To jest twój jednorazowy link!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete up to 20 messages at once." xml:space="preserve">
|
||||
<source>Delete up to 20 messages at once.</source>
|
||||
<target>Usuń do 20 wiadomości na raz.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete user profile?" xml:space="preserve">
|
||||
@@ -2160,6 +2186,7 @@ To jest twój jednorazowy link!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete without notification" xml:space="preserve">
|
||||
<source>Delete without notification</source>
|
||||
<target>Usuń bez powiadomienia</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Deleted" xml:space="preserve">
|
||||
@@ -2219,6 +2246,7 @@ To jest twój jednorazowy link!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Destination server address of %@ is incompatible with forwarding server %@ settings." xml:space="preserve">
|
||||
<source>Destination server address of %@ is incompatible with forwarding server %@ settings.</source>
|
||||
<target>Adres serwera docelowego %@ jest niekompatybilny z ustawieniami serwera przekazującego %@.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Destination server error: %@" xml:space="preserve">
|
||||
@@ -2228,6 +2256,7 @@ To jest twój jednorazowy link!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Destination server version of %@ is incompatible with forwarding server %@." xml:space="preserve">
|
||||
<source>Destination server version of %@ is incompatible with forwarding server %@.</source>
|
||||
<target>Wersja serwera docelowego %@ jest niekompatybilna z serwerem przekierowującym %@.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Detailed statistics" xml:space="preserve">
|
||||
@@ -2247,6 +2276,7 @@ To jest twój jednorazowy link!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer options" xml:space="preserve">
|
||||
<source>Developer options</source>
|
||||
<target>Opcje deweloperskie</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
@@ -2301,6 +2331,7 @@ To jest twój jednorazowy link!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disabled" xml:space="preserve">
|
||||
<source>Disabled</source>
|
||||
<target>Wyłączony</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappearing message" xml:space="preserve">
|
||||
@@ -2530,6 +2561,7 @@ To jest twój jednorazowy link!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enabled" xml:space="preserve">
|
||||
<source>Enabled</source>
|
||||
<target>Włączony</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enabled for" xml:space="preserve">
|
||||
@@ -2704,6 +2736,7 @@ To jest twój jednorazowy link!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
|
||||
<source>Error connecting to forwarding server %@. Please try later.</source>
|
||||
<target>Błąd połączenia z serwerem przekierowania %@. Spróbuj ponownie później.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating address" xml:space="preserve">
|
||||
@@ -3209,14 +3242,17 @@ To jest twój jednorazowy link!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve">
|
||||
<source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source>
|
||||
<target>Serwer przekazujący %@ nie mógł połączyć się z serwerem docelowym %@. Spróbuj ponownie później.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Forwarding server address is incompatible with network settings: %@." xml:space="preserve">
|
||||
<source>Forwarding server address is incompatible with network settings: %@.</source>
|
||||
<target>Adres serwera przekierowującego jest niekompatybilny z ustawieniami sieciowymi: %@.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Forwarding server version is incompatible with network settings: %@." xml:space="preserve">
|
||||
<source>Forwarding server version is incompatible with network settings: %@.</source>
|
||||
<target>Wersja serwera przekierowującego jest niekompatybilna z ustawieniami sieciowymi: %@.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Forwarding server: %@ Destination server error: %@" xml:space="preserve">
|
||||
@@ -3803,6 +3839,7 @@ Błąd: %2$@</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="It protects your IP address and connections." xml:space="preserve">
|
||||
<source>It protects your IP address and connections.</source>
|
||||
<target>Chroni Twój adres IP i połączenia.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve">
|
||||
@@ -3869,6 +3906,7 @@ To jest twój link do grupy %@!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Keep conversation" xml:space="preserve">
|
||||
<source>Keep conversation</source>
|
||||
<target>Zachowaj rozmowę</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Keep the app open to use it from desktop" xml:space="preserve">
|
||||
@@ -4048,10 +4086,12 @@ To jest twój link do grupy %@!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Media & file servers" xml:space="preserve">
|
||||
<source>Media & file servers</source>
|
||||
<target>Serwery mediów i plików</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Medium" xml:space="preserve">
|
||||
<source>Medium</source>
|
||||
<target>Średni</target>
|
||||
<note>blur media</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Member" xml:space="preserve">
|
||||
@@ -4141,6 +4181,7 @@ To jest twój link do grupy %@!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message servers" xml:space="preserve">
|
||||
<source>Message servers</source>
|
||||
<target>Serwery wiadomości</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message source remains private." xml:space="preserve">
|
||||
@@ -4355,6 +4396,7 @@ To jest twój link do grupy %@!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="New chat experience 🎉" xml:space="preserve">
|
||||
<source>New chat experience 🎉</source>
|
||||
<target>Nowe możliwości czatu 🎉</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New contact request" xml:space="preserve">
|
||||
@@ -4389,6 +4431,7 @@ To jest twój link do grupy %@!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="New media options" xml:space="preserve">
|
||||
<source>New media options</source>
|
||||
<target>Nowe opcje mediów</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New member role" xml:space="preserve">
|
||||
@@ -4483,6 +4526,7 @@ To jest twój link do grupy %@!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Nothing selected" xml:space="preserve">
|
||||
<source>Nothing selected</source>
|
||||
<target>Nic nie jest zaznaczone</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Notifications" xml:space="preserve">
|
||||
@@ -4560,6 +4604,7 @@ Wymaga włączenia VPN.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only delete conversation" xml:space="preserve">
|
||||
<source>Only delete conversation</source>
|
||||
<target>Usuń tylko rozmowę</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
||||
@@ -4799,10 +4844,12 @@ Wymaga włączenia VPN.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Play from the chat list." xml:space="preserve">
|
||||
<source>Play from the chat list.</source>
|
||||
<target>Odtwórz z listy czatów.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please ask your contact to enable calls." xml:space="preserve">
|
||||
<source>Please ask your contact to enable calls.</source>
|
||||
<target>Poproś kontakt o włącznie połączeń.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve">
|
||||
@@ -5108,6 +5155,7 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reachable chat toolbar" xml:space="preserve">
|
||||
<source>Reachable chat toolbar</source>
|
||||
<target>Osiągalny pasek narzędzi czatu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="React…" xml:space="preserve">
|
||||
@@ -5383,6 +5431,7 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reset all hints" xml:space="preserve">
|
||||
<source>Reset all hints</source>
|
||||
<target>Zresetuj wszystkie wskazówki</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reset all statistics" xml:space="preserve">
|
||||
@@ -5517,6 +5566,7 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Save and reconnect" xml:space="preserve">
|
||||
<source>Save and reconnect</source>
|
||||
<target>Zapisz i połącz ponownie</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Save and update group profile" xml:space="preserve">
|
||||
@@ -5681,6 +5731,7 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected %lld" xml:space="preserve">
|
||||
<source>Selected %lld</source>
|
||||
<target>Zaznaczono %lld</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
@@ -5750,6 +5801,7 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send message to enable calls." xml:space="preserve">
|
||||
<source>Send message to enable calls.</source>
|
||||
<target>Wyślij wiadomość aby włączyć połączenia.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve">
|
||||
@@ -6039,6 +6091,7 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share from other apps." xml:space="preserve">
|
||||
<source>Share from other apps.</source>
|
||||
<target>Udostępnij z innych aplikacji.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share link" xml:space="preserve">
|
||||
@@ -6053,6 +6106,7 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share to SimpleX" xml:space="preserve">
|
||||
<source>Share to SimpleX</source>
|
||||
<target>Udostępnij do SimpleX</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share with contacts" xml:space="preserve">
|
||||
@@ -6207,10 +6261,12 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Soft" xml:space="preserve">
|
||||
<source>Soft</source>
|
||||
<target>Łagodny</target>
|
||||
<note>blur media</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Some file(s) were not exported:" xml:space="preserve">
|
||||
<source>Some file(s) were not exported:</source>
|
||||
<target>Niektóre plik(i) nie zostały wyeksportowane:</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve">
|
||||
@@ -6220,6 +6276,7 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Some non-fatal errors occurred during import:" xml:space="preserve">
|
||||
<source>Some non-fatal errors occurred during import:</source>
|
||||
<target>Podczas importu wystąpiły niekrytyczne błędy:</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Somebody" xml:space="preserve">
|
||||
@@ -6319,6 +6376,7 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Strong" xml:space="preserve">
|
||||
<source>Strong</source>
|
||||
<target>Silne</target>
|
||||
<note>blur media</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Submit" xml:space="preserve">
|
||||
@@ -6358,6 +6416,7 @@ Włącz w ustawianiach *Sieć i serwery* .</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="TCP connection" xml:space="preserve">
|
||||
<source>TCP connection</source>
|
||||
<target>Połączenie TCP</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="TCP connection timeout" xml:space="preserve">
|
||||
@@ -6529,10 +6588,12 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be deleted for all members." xml:space="preserve">
|
||||
<source>The messages will be deleted for all members.</source>
|
||||
<target>Wiadomości zostaną usunięte dla wszystkich członków.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve">
|
||||
<source>The messages will be marked as moderated for all members.</source>
|
||||
<target>Wiadomości zostaną oznaczone jako moderowane dla wszystkich członków.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
@@ -6719,6 +6780,7 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</ta
|
||||
</trans-unit>
|
||||
<trans-unit id="Toggle chat list:" xml:space="preserve">
|
||||
<source>Toggle chat list:</source>
|
||||
<target>Przełącz listę czatów:</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Toggle incognito when connecting." xml:space="preserve">
|
||||
@@ -6728,6 +6790,7 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</ta
|
||||
</trans-unit>
|
||||
<trans-unit id="Toolbar opacity" xml:space="preserve">
|
||||
<source>Toolbar opacity</source>
|
||||
<target>Nieprzezroczystość paska narzędzi</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Total" xml:space="preserve">
|
||||
@@ -6914,6 +6977,7 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc
|
||||
</trans-unit>
|
||||
<trans-unit id="Update settings?" xml:space="preserve">
|
||||
<source>Update settings?</source>
|
||||
<target>Zaktualizować ustawienia?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve">
|
||||
@@ -7023,6 +7087,7 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc
|
||||
</trans-unit>
|
||||
<trans-unit id="Use the app with one hand." xml:space="preserve">
|
||||
<source>Use the app with one hand.</source>
|
||||
<target>Korzystaj z aplikacji jedną ręką.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="User profile" xml:space="preserve">
|
||||
@@ -7384,6 +7449,7 @@ Powtórzyć prośbę dołączenia?</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can change it in Appearance settings." xml:space="preserve">
|
||||
<source>You can change it in Appearance settings.</source>
|
||||
<target>Możesz to zmienić w ustawieniach wyglądu.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can create it later" xml:space="preserve">
|
||||
@@ -7423,6 +7489,7 @@ Powtórzyć prośbę dołączenia?</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can send messages to %@ from Archived contacts." xml:space="preserve">
|
||||
<source>You can send messages to %@ from Archived contacts.</source>
|
||||
<target>Możesz wysyłać wiadomości do %@ ze zarchiwizowanych kontaktów.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve">
|
||||
@@ -7452,6 +7519,7 @@ Powtórzyć prośbę dołączenia?</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can still view conversation with %@ in the list of chats." xml:space="preserve">
|
||||
<source>You can still view conversation with %@ in the list of chats.</source>
|
||||
<target>Nadal możesz przeglądać rozmowę z %@ na liście czatów.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve">
|
||||
@@ -7518,10 +7586,12 @@ Powtórzyć prośbę połączenia?</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="You may migrate the exported database." xml:space="preserve">
|
||||
<source>You may migrate the exported database.</source>
|
||||
<target>Możesz zmigrować wyeksportowaną bazy danych.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You may save the exported archive." xml:space="preserve">
|
||||
<source>You may save the exported archive.</source>
|
||||
<target>Możesz zapisać wyeksportowane archiwum.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve">
|
||||
@@ -7531,6 +7601,7 @@ Powtórzyć prośbę połączenia?</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="You need to allow your contact to call to be able to call them." xml:space="preserve">
|
||||
<source>You need to allow your contact to call to be able to call them.</source>
|
||||
<target>Aby móc dzwonić, musisz zezwolić kontaktowi na połączenia.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve">
|
||||
@@ -7842,6 +7913,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="call" xml:space="preserve">
|
||||
<source>call</source>
|
||||
<target>zadzwoń</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="call error" xml:space="preserve">
|
||||
@@ -8216,6 +8288,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invite" xml:space="preserve">
|
||||
<source>invite</source>
|
||||
<target>zaproś</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="invited" xml:space="preserve">
|
||||
@@ -8275,6 +8348,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="message" xml:space="preserve">
|
||||
<source>message</source>
|
||||
<target>wiadomość</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="message received" xml:space="preserve">
|
||||
@@ -8309,6 +8383,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="mute" xml:space="preserve">
|
||||
<source>mute</source>
|
||||
<target>wycisz</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="never" xml:space="preserve">
|
||||
@@ -8445,6 +8520,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="search" xml:space="preserve">
|
||||
<source>search</source>
|
||||
<target>szukaj</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="sec" xml:space="preserve">
|
||||
@@ -8533,6 +8609,7 @@ ostatnia otrzymana wiadomość: %2$@</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="unmute" xml:space="preserve">
|
||||
<source>unmute</source>
|
||||
<target>wyłącz wyciszenie</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="unprotected" xml:space="preserve">
|
||||
@@ -8582,6 +8659,7 @@ ostatnia otrzymana wiadomość: %2$@</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="video" xml:space="preserve">
|
||||
<source>video</source>
|
||||
<target>wideo</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="video call (not e2e encrypted)" xml:space="preserve">
|
||||
@@ -8762,14 +8840,17 @@ ostatnia otrzymana wiadomość: %2$@</target>
|
||||
<body>
|
||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||
<source>SimpleX SE</source>
|
||||
<target>SimpleX SE</target>
|
||||
<note>Bundle display name</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="CFBundleName" xml:space="preserve">
|
||||
<source>SimpleX SE</source>
|
||||
<target>SimpleX SE</target>
|
||||
<note>Bundle name</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="NSHumanReadableCopyright" xml:space="preserve">
|
||||
<source>Copyright © 2024 SimpleX Chat. All rights reserved.</source>
|
||||
<target>Copyright © 2024 SimpleX Chat. Wszelkie prawa zastrzeżone.</target>
|
||||
<note>Copyright (human-readable)</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
@@ -8781,150 +8862,187 @@ ostatnia otrzymana wiadomość: %2$@</target>
|
||||
<body>
|
||||
<trans-unit id="%@" xml:space="preserve">
|
||||
<source>%@</source>
|
||||
<target>%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App is locked!" xml:space="preserve">
|
||||
<source>App is locked!</source>
|
||||
<target>Aplikacja zablokowana!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<target>Anuluj</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<target>Nie można uzyskać dostępu do pęku kluczy aby zapisać hasło do bazy danych</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot forward message" xml:space="preserve">
|
||||
<source>Cannot forward message</source>
|
||||
<target>Nie można przekazać wiadomości</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Comment" xml:space="preserve">
|
||||
<source>Comment</source>
|
||||
<target>Komentarz</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Currently maximum supported file size is %@." xml:space="preserve">
|
||||
<source>Currently maximum supported file size is %@.</source>
|
||||
<target>Obecnie maksymalny obsługiwany rozmiar pliku to %@.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database downgrade required" xml:space="preserve">
|
||||
<source>Database downgrade required</source>
|
||||
<target>Wymagane obniżenie wersji bazy danych</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database encrypted!" xml:space="preserve">
|
||||
<source>Database encrypted!</source>
|
||||
<target>Baza danych zaszyfrowana!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database error" xml:space="preserve">
|
||||
<source>Database error</source>
|
||||
<target>Błąd bazy danych</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve">
|
||||
<source>Database passphrase is different from saved in the keychain.</source>
|
||||
<target>Hasło bazy danych jest inne niż zapisane w pęku kluczy.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
|
||||
<source>Database passphrase is required to open chat.</source>
|
||||
<target>Hasło do bazy danych jest wymagane do otwarcia czatu.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database upgrade required" xml:space="preserve">
|
||||
<source>Database upgrade required</source>
|
||||
<target>Wymagana aktualizacja bazy danych</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error preparing file" xml:space="preserve">
|
||||
<source>Error preparing file</source>
|
||||
<target>Błąd przygotowania pliku</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error preparing message" xml:space="preserve">
|
||||
<source>Error preparing message</source>
|
||||
<target>Błąd przygotowania wiadomości</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error: %@" xml:space="preserve">
|
||||
<source>Error: %@</source>
|
||||
<target>Błąd: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="File error" xml:space="preserve">
|
||||
<source>File error</source>
|
||||
<target>Błąd pliku</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Incompatible database version" xml:space="preserve">
|
||||
<source>Incompatible database version</source>
|
||||
<target>Niekompatybilna wersja bazy danych</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
|
||||
<source>Invalid migration confirmation</source>
|
||||
<target>Nieprawidłowe potwierdzenie migracji</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Keychain error" xml:space="preserve">
|
||||
<source>Keychain error</source>
|
||||
<target>Błąd pęku kluczy</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Large file!" xml:space="preserve">
|
||||
<source>Large file!</source>
|
||||
<target>Duży plik!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No active profile" xml:space="preserve">
|
||||
<source>No active profile</source>
|
||||
<target>Brak aktywnego profilu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Ok" xml:space="preserve">
|
||||
<source>Ok</source>
|
||||
<target>Ok</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open the app to downgrade the database." xml:space="preserve">
|
||||
<source>Open the app to downgrade the database.</source>
|
||||
<target>Otwórz aplikację aby obniżyć wersję bazy danych.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open the app to upgrade the database." xml:space="preserve">
|
||||
<source>Open the app to upgrade the database.</source>
|
||||
<target>Otwórz aplikację aby zaktualizować bazę danych.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Passphrase" xml:space="preserve">
|
||||
<source>Passphrase</source>
|
||||
<target>Hasło</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve">
|
||||
<source>Please create a profile in the SimpleX app</source>
|
||||
<target>Proszę utworzyć profil w aplikacji SimpleX</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve">
|
||||
<source>Selected chat preferences prohibit this message.</source>
|
||||
<target>Wybrane preferencje czatu zabraniają tej wiadomości.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sending a message takes longer than expected." xml:space="preserve">
|
||||
<source>Sending a message takes longer than expected.</source>
|
||||
<target>Wysłanie wiadomości trwa dłużej niż oczekiwano.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sending message…" xml:space="preserve">
|
||||
<source>Sending message…</source>
|
||||
<target>Wysyłanie wiadomości…</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share" xml:space="preserve">
|
||||
<source>Share</source>
|
||||
<target>Udostępnij</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Slow network?" xml:space="preserve">
|
||||
<source>Slow network?</source>
|
||||
<target>Wolna sieć?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown database error: %@" xml:space="preserve">
|
||||
<source>Unknown database error: %@</source>
|
||||
<target>Nieznany błąd bazy danych: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unsupported format" xml:space="preserve">
|
||||
<source>Unsupported format</source>
|
||||
<target>Niewspierany format</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wait" xml:space="preserve">
|
||||
<source>Wait</source>
|
||||
<target>Czekaj</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wrong database passphrase" xml:space="preserve">
|
||||
<source>Wrong database passphrase</source>
|
||||
<target>Nieprawidłowe hasło bazy danych</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve">
|
||||
<source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source>
|
||||
<target>Możesz zezwolić na udostępnianie w ustawieniach Prywatność i bezpieczeństwo / Blokada SimpleX.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
|
||||
@@ -5234,6 +5234,274 @@ Isso pode acontecer por causa de algum bug ou quando a conexão está comprometi
|
||||
<target state="translated">%1$@ em %2$@:</target>
|
||||
<note>copied message info, <sender> at <time></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no">
|
||||
<source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target state="translated">Permitir que seus contatos deletem mensagens enviadas de maneira irreversível. (24 horas)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ downloaded" xml:space="preserve" approved="no">
|
||||
<source>%@ downloaded</source>
|
||||
<target state="translated">baixado</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ uploaded" xml:space="preserve" approved="no">
|
||||
<source>%@ uploaded</source>
|
||||
<target state="translated">transferido</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="A new random profile will be shared." xml:space="preserve" approved="no">
|
||||
<source>A new random profile will be shared.</source>
|
||||
<target state="translated">Um novo perfil aleatório será compartilhado.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Camera not available" xml:space="preserve" approved="no">
|
||||
<source>Camera not available</source>
|
||||
<target state="translated">Câmera indisponível</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Admins can block a member for all." xml:space="preserve" approved="no">
|
||||
<source>Admins can block a member for all.</source>
|
||||
<target state="translated">Administradores podem bloquear um membro para todos.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no">
|
||||
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target state="translated">Permitir que mensagens enviadas sejam deletadas de maneira irreversível. (24 horas)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Apply" xml:space="preserve" approved="no">
|
||||
<source>Apply</source>
|
||||
<target state="translated">Aplicar</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accent" xml:space="preserve" approved="no">
|
||||
<source>Accent</source>
|
||||
<target state="translated">Esquema</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accept connection request?" xml:space="preserve" approved="no">
|
||||
<source>Accept connection request?</source>
|
||||
<target state="translated">Aceitar solicitação de conexão?</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Active connections" xml:space="preserve" approved="no">
|
||||
<source>Active connections</source>
|
||||
<target state="translated">Conexões ativas</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Add contact" xml:space="preserve" approved="no">
|
||||
<source>Add contact</source>
|
||||
<target state="translated">Adicionar contato</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Additional accent" xml:space="preserve" approved="no">
|
||||
<source>Additional accent</source>
|
||||
<target state="translated">Esquema adicional</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve" approved="no">
|
||||
<source>All new messages from %@ will be hidden!</source>
|
||||
<target state="translated">Todas as novas mensagens de %@ serão ocultas!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="All profiles" xml:space="preserve" approved="no">
|
||||
<source>All profiles</source>
|
||||
<target state="translated">Todos perfis</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow calls?" xml:space="preserve" approved="no">
|
||||
<source>Allow calls?</source>
|
||||
<target state="translated">Permitir chamadas?</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Archive contacts to chat later." xml:space="preserve" approved="no">
|
||||
<source>Archive contacts to chat later.</source>
|
||||
<target state="translated">Arquivar contatos para conversar depois.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Blur media" xml:space="preserve" approved="no">
|
||||
<source>Blur media</source>
|
||||
<target state="translated">Censurar mídia</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Calls prohibited!" xml:space="preserve" approved="no">
|
||||
<source>Calls prohibited!</source>
|
||||
<target state="translated">Chamadas proibidas!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't call contact" xml:space="preserve" approved="no">
|
||||
<source>Can't call contact</source>
|
||||
<target state="translated">Não foi possível ligar para o contato</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld messages marked deleted" xml:space="preserve" approved="no">
|
||||
<source>%lld messages marked deleted</source>
|
||||
<target state="translated">mensagens deletadas</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="0 sec" xml:space="preserve" approved="no">
|
||||
<source>0 sec</source>
|
||||
<target state="translated">0 seg</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld messages blocked" xml:space="preserve" approved="no">
|
||||
<source>%lld messages blocked</source>
|
||||
<target state="translated">mensagens bloqueadas</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld messages blocked by admin" xml:space="preserve" approved="no">
|
||||
<source>%lld messages blocked by admin</source>
|
||||
<target state="translated">mensagens bloqueadas pelo administrador</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve" approved="no">
|
||||
<source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source>
|
||||
<target state="translated">**Nota**: usar o mesmo banco de dados em dois dispositivos irá quebrar a desencriptação das mensagens de suas conexões como uma medida de segurança.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve" approved="no">
|
||||
<source>- more stable message delivery.
|
||||
- a bit better groups.
|
||||
- and more!</source>
|
||||
<target state="translated">- entrega de mensagens mais estável.
|
||||
- grupos melhorados.
|
||||
- e muito mais!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve" approved="no">
|
||||
<source>All messages will be deleted - this cannot be undone!</source>
|
||||
<target state="translated">Todas as mensagens serão deletadas - isto não pode ser desfeito!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to send files and media." xml:space="preserve" approved="no">
|
||||
<source>Allow to send files and media.</source>
|
||||
<target state="translated">Permitir o envio de arquivos e mídia.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to send SimpleX links." xml:space="preserve" approved="no">
|
||||
<source>Allow to send SimpleX links.</source>
|
||||
<target state="translated">Permitir envio de links SimpleX.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Block for all" xml:space="preserve" approved="no">
|
||||
<source>Block for all</source>
|
||||
<target state="translated">Bloquear para todos</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Block member" xml:space="preserve" approved="no">
|
||||
<source>Block member</source>
|
||||
<target state="translated">Bloquear membro</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Blocked by admin" xml:space="preserve" approved="no">
|
||||
<source>Blocked by admin</source>
|
||||
<target state="translated">Bloqueado por um administrador</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Block group members" xml:space="preserve" approved="no">
|
||||
<source>Block group members</source>
|
||||
<target state="translated">Bloquear membros de grupo</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Block member for all?" xml:space="preserve" approved="no">
|
||||
<source>Block member for all?</source>
|
||||
<target state="translated">Bloquear membro para todos?</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Block member?" xml:space="preserve" approved="no">
|
||||
<source>Block member?</source>
|
||||
<target state="translated">Bloquear membro?</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no">
|
||||
<source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source>
|
||||
<target state="translated">Você e seu contato podem apagar mensagens enviadas de maneira irreversível. (24 horas)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't call member" xml:space="preserve" approved="no">
|
||||
<source>Can't call member</source>
|
||||
<target state="translated">Não foi possível ligar para este membro</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't message member" xml:space="preserve" approved="no">
|
||||
<source>Can't message member</source>
|
||||
<target state="translated">Não foi possível enviar mensagem para este membro</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel migration" xml:space="preserve" approved="no">
|
||||
<source>Cancel migration</source>
|
||||
<target state="translated">Cancelar migração</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Abort" xml:space="preserve" approved="no">
|
||||
<source>Abort</source>
|
||||
<target state="translated">Abortar</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Abort changing address" xml:space="preserve" approved="no">
|
||||
<source>Abort changing address</source>
|
||||
<target state="translated">Abortar troca de endereço</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Abort changing address?" xml:space="preserve" approved="no">
|
||||
<source>Abort changing address?</source>
|
||||
<target state="translated">Abortar troca de endereço?</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="- optionally notify deleted contacts. - profile names with spaces. - and more!" xml:space="preserve" approved="no">
|
||||
<source>- optionally notify deleted contacts.
|
||||
- profile names with spaces.
|
||||
- and more!</source>
|
||||
<target state="translated">- notificar contatos apagados de maneira opcional.
|
||||
- nome de perfil com espaços.
|
||||
- e muito mais!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sharing" xml:space="preserve" approved="no">
|
||||
<source>Allow sharing</source>
|
||||
<target state="translated">Permitir compartilhamento</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Block" xml:space="preserve" approved="no">
|
||||
<source>Block</source>
|
||||
<target state="translated">Bloquear</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Additional accent 2" xml:space="preserve" approved="no">
|
||||
<source>Additional accent 2</source>
|
||||
<target state="translated">Esquema adicional 2</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve" approved="no">
|
||||
<source>Address change will be aborted. Old receiving address will be used.</source>
|
||||
<target state="translated">Alteração de endereço será abortada. O endereço antigo será utilizado.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Advanced settings" xml:space="preserve" approved="no">
|
||||
<source>Advanced settings</source>
|
||||
<target state="translated">Configurações avançadas</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="All data is private to your device." xml:space="preserve" approved="no">
|
||||
<source>All data is private to your device.</source>
|
||||
<target state="translated">Toda informação é privada em seu dispositivo.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve" approved="no">
|
||||
<source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source>
|
||||
<target state="translated">Todos os seus contatos, conversas e arquivos serão encriptados e enviados em pedaços para nós XFTP.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve" approved="no">
|
||||
<source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source>
|
||||
<target state="translated">Permitir deletar mensagens de maneira irreversível apenas se seu contato permitir para você. (24 horas)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Already connecting!" xml:space="preserve" approved="no">
|
||||
<source>Already connecting!</source>
|
||||
<target state="translated">Já está conectando!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Already joining the group!" xml:space="preserve" approved="no">
|
||||
<source>Already joining the group!</source>
|
||||
<target state="translated">Já está entrando no grupo!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Always use private routing." xml:space="preserve" approved="no">
|
||||
<source>Always use private routing.</source>
|
||||
<target state="translated">Sempre use rotas privadas.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Apply to" xml:space="preserve" approved="no">
|
||||
<source>Apply to</source>
|
||||
<target state="translated">Aplicar em</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Archiving database" xml:space="preserve" approved="no">
|
||||
<source>Archiving database</source>
|
||||
<target state="translated">Arquivando banco de dados</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Black" xml:space="preserve" approved="no">
|
||||
<source>Black</source>
|
||||
<target state="translated">Preto</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot forward message" xml:space="preserve" approved="no">
|
||||
<source>Cannot forward message</source>
|
||||
<target state="translated">Não é possível encaminhar mensagem</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="(new)" xml:space="preserve" approved="no">
|
||||
<source>(new)</source>
|
||||
<target state="translated">(novo)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="(this device v%@)" xml:space="preserve" approved="no">
|
||||
<source>(this device v%@)</source>
|
||||
<target state="translated">este dispositivo</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Add contact**: to create a new invitation link, or connect via a link you received." xml:space="preserve" approved="no">
|
||||
<source>**Add contact**: to create a new invitation link, or connect via a link you received.</source>
|
||||
<target state="translated">**Adicionar contato**: criar um novo link de convite ou conectar via um link que você recebeu.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Create group**: to create a new group." xml:space="preserve" approved="no">
|
||||
<source>**Create group**: to create a new group.</source>
|
||||
<target state="translated">**Criar grupo**: criar um novo grupo.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Warning**: the archive will be removed." xml:space="preserve" approved="no">
|
||||
<source>**Warning**: the archive will be removed.</source>
|
||||
<target state="translated">**Aviso**: o arquivo será removido.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="A few more things" xml:space="preserve" approved="no">
|
||||
<source>A few more things</source>
|
||||
<target state="translated">E mais algumas coisas</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Archived contacts" xml:space="preserve" approved="no">
|
||||
<source>Archived contacts</source>
|
||||
<target state="translated">Contatos arquivados</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="pt-BR" datatype="plaintext">
|
||||
|
||||
@@ -1418,7 +1418,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Color chats with the new themes." xml:space="preserve">
|
||||
<source>Color chats with the new themes.</source>
|
||||
<target>Добавьте цвета к чатам в настройках тем.</target>
|
||||
<target>Добавьте цвета к чатам в настройках.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Color mode" xml:space="preserve">
|
||||
@@ -2176,7 +2176,7 @@ This is your own one-time link!</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete up to 20 messages at once." xml:space="preserve">
|
||||
<source>Delete up to 20 messages at once.</source>
|
||||
<target>Удаляйте до 20 сообщений одновременно.</target>
|
||||
<target>Удаляйте до 20 сообщений за раз.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete user profile?" xml:space="preserve">
|
||||
@@ -4988,7 +4988,7 @@ Error: %@</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="Private message routing 🚀" xml:space="preserve">
|
||||
<source>Private message routing 🚀</source>
|
||||
<target>Конфиденциальная доставка сообщений 🚀</target>
|
||||
<target>Конфиденциальная доставка 🚀</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Private notes" xml:space="preserve">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -314,7 +314,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="About SimpleX Chat" xml:space="preserve" approved="no">
|
||||
<source>About SimpleX Chat</source>
|
||||
<target state="translated">關於 SimpleX 對話</target>
|
||||
<target state="translated">關於 SimpleX Chat</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accent color" xml:space="preserve" approved="no">
|
||||
@@ -445,7 +445,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve" approved="no">
|
||||
<source>Allow your contacts to send disappearing messages.</source>
|
||||
<target state="translated">允許你的聯絡人傳送自動銷毀的訊息。</target>
|
||||
<target state="translated">允許您的聯絡人傳送限時訊息。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow your contacts to send voice messages." xml:space="preserve" approved="no">
|
||||
@@ -5898,6 +5898,230 @@ It can happen because of some bug or when the connection is compromised.</source
|
||||
<target state="translated">%@ 和 %@ 已連接</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ downloaded" xml:space="preserve" approved="no">
|
||||
<source>%@ downloaded</source>
|
||||
<target state="translated">%@ 下載</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ uploaded" xml:space="preserve" approved="no">
|
||||
<source>%@ uploaded</source>
|
||||
<target state="translated">%@ 上傳</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Abort" xml:space="preserve" approved="no">
|
||||
<source>Abort</source>
|
||||
<target state="translated">中止</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Create group**: to create a new group." xml:space="preserve" approved="no">
|
||||
<source>**Create group**: to create a new group.</source>
|
||||
<target state="translated">**創建群組**: 創建一個新的群組。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Abort changing address" xml:space="preserve" approved="no">
|
||||
<source>Abort changing address</source>
|
||||
<target state="translated">中止更改地址</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accept connection request?" xml:space="preserve" approved="no">
|
||||
<source>Accept connection request?</source>
|
||||
<target state="translated">接受連線請求?</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Camera not available" xml:space="preserve" approved="no">
|
||||
<source>Camera not available</source>
|
||||
<target state="translated">相機不可用</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve" approved="no">
|
||||
<source>All messages will be deleted - this cannot be undone!</source>
|
||||
<target state="translated">所有訊息都將被刪除 - 這不能還原!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve" approved="no">
|
||||
<source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source>
|
||||
<target state="translated">只有你的聯絡人允許的情況下,才允許不可逆地將訊息刪除。(24小時)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no">
|
||||
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target state="translated">允許將不可撤銷的訊息刪除。(24小時)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no">
|
||||
<source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target state="translated">允許您的聯絡人不可復原地刪除已傳送的訊息。(24小時)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Bad desktop address" xml:space="preserve" approved="no">
|
||||
<source>Bad desktop address</source>
|
||||
<target state="translated">無效的桌面地址</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error decrypting file" xml:space="preserve" approved="no">
|
||||
<source>Error decrypting file</source>
|
||||
<target state="translated">解密檔案時出錯</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Add contact" xml:space="preserve" approved="no">
|
||||
<source>Add contact</source>
|
||||
<target state="translated">新增聯絡人</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Advanced settings" xml:space="preserve" approved="no">
|
||||
<source>Advanced settings</source>
|
||||
<target state="translated">進階設定</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow calls?" xml:space="preserve" approved="no">
|
||||
<source>Allow calls?</source>
|
||||
<target state="translated">允許通話?</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to send files and media." xml:space="preserve" approved="no">
|
||||
<source>Allow to send files and media.</source>
|
||||
<target state="translated">允許傳送檔案和媒體。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Already joining the group!" xml:space="preserve" approved="no">
|
||||
<source>Already joining the group!</source>
|
||||
<target state="translated">已加入群組!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="App data migration" xml:space="preserve" approved="no">
|
||||
<source>App data migration</source>
|
||||
<target state="translated">應用資料轉移</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Apply" xml:space="preserve" approved="no">
|
||||
<source>Apply</source>
|
||||
<target state="translated">應用</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Apply to" xml:space="preserve" approved="no">
|
||||
<source>Apply to</source>
|
||||
<target state="translated">應用到</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Archive and upload" xml:space="preserve" approved="no">
|
||||
<source>Archive and upload</source>
|
||||
<target state="translated">儲存並上傳</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Block" xml:space="preserve" approved="no">
|
||||
<source>Block</source>
|
||||
<target state="translated">封鎖</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Block group members" xml:space="preserve" approved="no">
|
||||
<source>Block group members</source>
|
||||
<target state="translated">封鎖群組成員</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Block member" xml:space="preserve" approved="no">
|
||||
<source>Block member</source>
|
||||
<target state="translated">封鎖成員</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve" approved="no">
|
||||
<source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source>
|
||||
<target state="translated">保加利亞語、芬蘭語、泰語和烏克蘭語——感謝使用者們和[Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't call member" xml:space="preserve" approved="no">
|
||||
<source>Can't call member</source>
|
||||
<target state="translated">無法與成員通話</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't message member" xml:space="preserve" approved="no">
|
||||
<source>Can't message member</source>
|
||||
<target state="translated">無法傳送訊息給成員</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel migration" xml:space="preserve" approved="no">
|
||||
<source>Cancel migration</source>
|
||||
<target state="translated">取消遷移</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat database exported" xml:space="preserve" approved="no">
|
||||
<source>Chat database exported</source>
|
||||
<target state="translated">導出聊天數據庫</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="0 sec" xml:space="preserve" approved="no">
|
||||
<source>0 sec</source>
|
||||
<target state="translated">0 秒</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve" approved="no">
|
||||
<source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source>
|
||||
<target state="translated">你的所有聯絡人、對話和文件將被安全加密並切塊上傳到設置的 XFTP 中繼。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve" approved="no">
|
||||
<source>Address change will be aborted. Old receiving address will be used.</source>
|
||||
<target state="translated">將取消地址更改。將使用舊聯絡地址。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Archiving database" xml:space="preserve" approved="no">
|
||||
<source>Archiving database</source>
|
||||
<target state="translated">正在儲存資料庫</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cellular" xml:space="preserve" approved="no">
|
||||
<source>Cellular</source>
|
||||
<target state="translated">行動網路</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld members" xml:space="preserve" approved="no">
|
||||
<source>%@, %@ and %lld members</source>
|
||||
<target state="translated">%@, %@ 和 %lld 成員</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld messages marked deleted" xml:space="preserve" approved="no">
|
||||
<source>%lld messages marked deleted</source>
|
||||
<target state="translated">%lld 條訊息已刪除</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Already connecting!" xml:space="preserve" approved="no">
|
||||
<source>Already connecting!</source>
|
||||
<target state="translated">已連接!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Block member?" xml:space="preserve" approved="no">
|
||||
<source>Block member?</source>
|
||||
<target state="translated">封鎖成員?</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="(new)" xml:space="preserve" approved="no">
|
||||
<source>(new)</source>
|
||||
<target state="translated">(新)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve" approved="no">
|
||||
<source>%@, %@ and %lld other members connected</source>
|
||||
<target state="translated">%@, %@ 和 %lld 個成員已連接</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="A few more things" xml:space="preserve" approved="no">
|
||||
<source>A few more things</source>
|
||||
<target state="translated">其他</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show last messages" xml:space="preserve" approved="no">
|
||||
<source>Show last messages</source>
|
||||
<target state="translated">顯示最新的訊息</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="App encrypts new local files (except videos)." xml:space="preserve" approved="no">
|
||||
<source>App encrypts new local files (except videos).</source>
|
||||
<target state="translated">應用程式將為新的本機文件(影片除外)加密。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Better groups" xml:space="preserve" approved="no">
|
||||
<source>Better groups</source>
|
||||
<target state="translated">更加的群組</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld new interface languages" xml:space="preserve" approved="no">
|
||||
<source>%lld new interface languages</source>
|
||||
<target state="translated">%lld 種新的介面語言</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Blocked by admin" xml:space="preserve" approved="no">
|
||||
<source>Blocked by admin</source>
|
||||
<target state="translated">由管理員封鎖</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no">
|
||||
<source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source>
|
||||
<target state="translated">您與您的聯絡人都可以不可復原地删除已傳送的訊息。(24小時)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt local files" xml:space="preserve" approved="no">
|
||||
<source>Encrypt local files</source>
|
||||
<target state="translated">加密本機檔案</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve" approved="no">
|
||||
<source>- more stable message delivery.
|
||||
- a bit better groups.
|
||||
- and more!</source>
|
||||
<target state="translated">- 更穩定的傳送!
|
||||
- 更好的社群!
|
||||
- 以及更多!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="- optionally notify deleted contacts. - profile names with spaces. - and more!" xml:space="preserve" approved="no">
|
||||
<source>- optionally notify deleted contacts.
|
||||
- profile names with spaces.
|
||||
- and more!</source>
|
||||
<target state="translated">- 可選擇通知已刪除的聯絡人
|
||||
- 帶空格的共人資料名稱。
|
||||
-以及更多!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Abort changing address?" xml:space="preserve" approved="no">
|
||||
<source>Abort changing address?</source>
|
||||
<target state="translated">中止更改地址?</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to send SimpleX links." xml:space="preserve" approved="no">
|
||||
<source>Allow to send SimpleX links.</source>
|
||||
<target state="translated">允許傳送 SimpleX 連結。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Background" xml:space="preserve" approved="no">
|
||||
<source>Background</source>
|
||||
<target state="translated">後台</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="zh-Hant" datatype="plaintext">
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/*
|
||||
InfoPlist.strings
|
||||
SimpleX
|
||||
/* Bundle display name */
|
||||
"CFBundleDisplayName" = "SimpleX SE";
|
||||
|
||||
/* Bundle name */
|
||||
"CFBundleName" = "SimpleX SE";
|
||||
|
||||
/* Copyright (human-readable) */
|
||||
"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. Wszelkie prawa zastrzeżone.";
|
||||
|
||||
Created by EP on 30/07/2024.
|
||||
Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,111 @@
|
||||
/*
|
||||
Localizable.strings
|
||||
SimpleX
|
||||
/* No comment provided by engineer. */
|
||||
"%@" = "%@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"App is locked!" = "Aplikacja zablokowana!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Cancel" = "Anuluj";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Cannot access keychain to save database password" = "Nie można uzyskać dostępu do pęku kluczy aby zapisać hasło do bazy danych";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Cannot forward message" = "Nie można przekazać wiadomości";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Comment" = "Komentarz";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Currently maximum supported file size is %@." = "Obecnie maksymalny obsługiwany rozmiar pliku to %@.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database downgrade required" = "Wymagane obniżenie wersji bazy danych";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database encrypted!" = "Baza danych zaszyfrowana!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database error" = "Błąd bazy danych";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database passphrase is different from saved in the keychain." = "Hasło bazy danych jest inne niż zapisane w pęku kluczy.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database passphrase is required to open chat." = "Hasło do bazy danych jest wymagane do otwarcia czatu.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database upgrade required" = "Wymagana aktualizacja bazy danych";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error preparing file" = "Błąd przygotowania pliku";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error preparing message" = "Błąd przygotowania wiadomości";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error: %@" = "Błąd: %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"File error" = "Błąd pliku";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Incompatible database version" = "Niekompatybilna wersja bazy danych";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Invalid migration confirmation" = "Nieprawidłowe potwierdzenie migracji";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Keychain error" = "Błąd pęku kluczy";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Large file!" = "Duży plik!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No active profile" = "Brak aktywnego profilu";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Ok" = "Ok";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Open the app to downgrade the database." = "Otwórz aplikację aby obniżyć wersję bazy danych.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Open the app to upgrade the database." = "Otwórz aplikację aby zaktualizować bazę danych.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Passphrase" = "Hasło";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Please create a profile in the SimpleX app" = "Proszę utworzyć profil w aplikacji SimpleX";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Selected chat preferences prohibit this message." = "Wybrane preferencje czatu zabraniają tej wiadomości.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Sending a message takes longer than expected." = "Wysłanie wiadomości trwa dłużej niż oczekiwano.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Sending message…" = "Wysyłanie wiadomości…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Share" = "Udostępnij";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Slow network?" = "Wolna sieć?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Unknown database error: %@" = "Nieznany błąd bazy danych: %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Unsupported format" = "Niewspierany format";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Wait" = "Czekaj";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Wrong database passphrase" = "Nieprawidłowe hasło bazy danych";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Możesz zezwolić na udostępnianie w ustawieniach Prywatność i bezpieczeństwo / Blokada SimpleX.";
|
||||
|
||||
Created by EP on 30/07/2024.
|
||||
Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/*
|
||||
InfoPlist.strings
|
||||
SimpleX
|
||||
/* Bundle display name */
|
||||
"CFBundleDisplayName" = "SimpleX SE";
|
||||
|
||||
/* Bundle name */
|
||||
"CFBundleName" = "SimpleX SE";
|
||||
|
||||
/* Copyright (human-readable) */
|
||||
"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. Всі права захищені.";
|
||||
|
||||
Created by EP on 30/07/2024.
|
||||
Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,111 @@
|
||||
/*
|
||||
Localizable.strings
|
||||
SimpleX
|
||||
/* No comment provided by engineer. */
|
||||
"%@" = "%@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"App is locked!" = "Додаток заблоковано!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Cancel" = "Скасувати";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Cannot access keychain to save database password" = "Не вдається отримати доступ до зв'язки ключів для збереження пароля до бази даних";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Cannot forward message" = "Неможливо переслати повідомлення";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Comment" = "Коментар";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Currently maximum supported file size is %@." = "Наразі максимальний підтримуваний розмір файлу - %@.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database downgrade required" = "Потрібне оновлення бази даних";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database encrypted!" = "База даних зашифрована!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database error" = "Помилка в базі даних";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database passphrase is different from saved in the keychain." = "Парольна фраза бази даних відрізняється від збереженої у в’язці ключів.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database passphrase is required to open chat." = "Для відкриття чату потрібно ввести пароль до бази даних.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Database upgrade required" = "Потрібне оновлення бази даних";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error preparing file" = "Помилка підготовки файлу";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error preparing message" = "Повідомлення про підготовку до помилки";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error: %@" = "Помилка: %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"File error" = "Помилка файлу";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Incompatible database version" = "Несумісна версія бази даних";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Invalid migration confirmation" = "Недійсне підтвердження міграції";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Keychain error" = "Помилка зв'язки ключів";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Large file!" = "Великий файл!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No active profile" = "Немає активного профілю";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Ok" = "Гаразд";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Open the app to downgrade the database." = "Відкрийте програму, щоб знизити версію бази даних.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Open the app to upgrade the database." = "Відкрийте програму, щоб оновити базу даних.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Passphrase" = "Парольна фраза";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Please create a profile in the SimpleX app" = "Будь ласка, створіть профіль у додатку SimpleX";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Selected chat preferences prohibit this message." = "Вибрані налаштування чату забороняють це повідомлення.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Sending a message takes longer than expected." = "Надсилання повідомлення займає більше часу, ніж очікувалося.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Sending message…" = "Надсилаю повідомлення…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Share" = "Поділіться";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Slow network?" = "Повільна мережа?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Unknown database error: %@" = "Невідома помилка бази даних: %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Unsupported format" = "Непідтримуваний формат";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Wait" = "Зачекай";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Wrong database passphrase" = "Неправильна ключова фраза до бази даних";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Ви можете дозволити спільний доступ у налаштуваннях Конфіденційність і безпека / SimpleX Lock.";
|
||||
|
||||
Created by EP on 30/07/2024.
|
||||
Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
*/
|
||||
|
||||
@@ -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,15 +216,15 @@
|
||||
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 */; };
|
||||
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 */; };
|
||||
E5DCF9982C5906FF007928CC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9962C5906FF007928CC /* InfoPlist.strings */; };
|
||||
E5E218532C6D4C0F0013B4C6 /* libHSsimplex-chat-6.0.0.8-9fvDFLivFrv8AINTqPH03q-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E2184E2C6D4C0F0013B4C6 /* libHSsimplex-chat-6.0.0.8-9fvDFLivFrv8AINTqPH03q-ghc9.6.3.a */; };
|
||||
E5E218542C6D4C0F0013B4C6 /* libHSsimplex-chat-6.0.0.8-9fvDFLivFrv8AINTqPH03q.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E2184F2C6D4C0F0013B4C6 /* libHSsimplex-chat-6.0.0.8-9fvDFLivFrv8AINTqPH03q.a */; };
|
||||
E5E218552C6D4C0F0013B4C6 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E218502C6D4C0F0013B4C6 /* libgmp.a */; };
|
||||
E5E218562C6D4C0F0013B4C6 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E218512C6D4C0F0013B4C6 /* libffi.a */; };
|
||||
E5E218572C6D4C0F0013B4C6 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E218522C6D4C0F0013B4C6 /* libgmpxx.a */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -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,6 +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>"; };
|
||||
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>"; };
|
||||
@@ -602,11 +611,6 @@
|
||||
E5DCF9A62C590731007928CC /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
E5DCF9A72C590732007928CC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
E5DCF9A82C590732007928CC /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
E5E2184E2C6D4C0F0013B4C6 /* libHSsimplex-chat-6.0.0.8-9fvDFLivFrv8AINTqPH03q-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.8-9fvDFLivFrv8AINTqPH03q-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
E5E2184F2C6D4C0F0013B4C6 /* libHSsimplex-chat-6.0.0.8-9fvDFLivFrv8AINTqPH03q.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.8-9fvDFLivFrv8AINTqPH03q.a"; sourceTree = "<group>"; };
|
||||
E5E218502C6D4C0F0013B4C6 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
E5E218512C6D4C0F0013B4C6 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
E5E218522C6D4C0F0013B4C6 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -645,14 +649,14 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
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 */,
|
||||
E5E218542C6D4C0F0013B4C6 /* libHSsimplex-chat-6.0.0.8-9fvDFLivFrv8AINTqPH03q.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 */,
|
||||
E5E218572C6D4C0F0013B4C6 /* libgmpxx.a in Frameworks */,
|
||||
E5E218562C6D4C0F0013B4C6 /* libffi.a in Frameworks */,
|
||||
E5E218552C6D4C0F0013B4C6 /* libgmp.a in Frameworks */,
|
||||
E5E218532C6D4C0F0013B4C6 /* libHSsimplex-chat-6.0.0.8-9fvDFLivFrv8AINTqPH03q-ghc9.6.3.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -729,11 +733,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E5E218512C6D4C0F0013B4C6 /* libffi.a */,
|
||||
E5E218502C6D4C0F0013B4C6 /* libgmp.a */,
|
||||
E5E218522C6D4C0F0013B4C6 /* libgmpxx.a */,
|
||||
E5E2184E2C6D4C0F0013B4C6 /* libHSsimplex-chat-6.0.0.8-9fvDFLivFrv8AINTqPH03q-ghc9.6.3.a */,
|
||||
E5E2184F2C6D4C0F0013B4C6 /* libHSsimplex-chat-6.0.0.8-9fvDFLivFrv8AINTqPH03q.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 = 233;
|
||||
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.1;
|
||||
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 = 233;
|
||||
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.1;
|
||||
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 = 233;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 6.0.1;
|
||||
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 = 233;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 6.0.1;
|
||||
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 = 233;
|
||||
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.1;
|
||||
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 = 233;
|
||||
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.1;
|
||||
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 = 233;
|
||||
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.1;
|
||||
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 = 233;
|
||||
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.1;
|
||||
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 = 233;
|
||||
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.1;
|
||||
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 = 233;
|
||||
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.1;
|
||||
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()
|
||||
@@ -537,7 +538,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case networkConfig(networkConfig: NetCfg)
|
||||
case contactInfo(user: UserRef, contact: Contact, connectionStats_: ConnectionStats?, customUserProfile: Profile?)
|
||||
case groupMemberInfo(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?)
|
||||
case queueInfo(user: UserRef, rcvMsgInfo: RcvMsgInfo?, queueInfo: QueueInfo)
|
||||
case queueInfo(user: UserRef, rcvMsgInfo: RcvMsgInfo?, queueInfo: ServerQueueInfo)
|
||||
case contactSwitchStarted(user: UserRef, contact: Contact, connectionStats: ConnectionStats)
|
||||
case groupMemberSwitchStarted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats)
|
||||
case contactSwitchAborted(user: UserRef, contact: Contact, connectionStats: ConnectionStats)
|
||||
@@ -1094,12 +1095,12 @@ public enum GroupLinkPlan: Decodable, Hashable {
|
||||
case known(groupInfo: GroupInfo)
|
||||
}
|
||||
|
||||
struct NewUser: Encodable, Hashable {
|
||||
struct NewUser: Encodable {
|
||||
var profile: Profile?
|
||||
var pastTimestamp: Bool
|
||||
}
|
||||
|
||||
public enum ChatPagination: Hashable {
|
||||
public enum ChatPagination {
|
||||
case last(count: Int)
|
||||
case after(chatItemId: Int64, count: Int)
|
||||
case before(chatItemId: Int64, count: Int)
|
||||
@@ -1315,7 +1316,7 @@ public struct ServerAddress: Decodable {
|
||||
)
|
||||
}
|
||||
|
||||
public struct NetCfg: Codable, Equatable, Hashable {
|
||||
public struct NetCfg: Codable, Equatable {
|
||||
public var socksProxy: String? = nil
|
||||
var socksMode: SocksMode = .always
|
||||
public var hostMode: HostMode = .publicHost
|
||||
@@ -1369,18 +1370,18 @@ public struct NetCfg: Codable, Equatable, Hashable {
|
||||
public var enableKeepAlive: Bool { tcpKeepAlive != nil }
|
||||
}
|
||||
|
||||
public enum HostMode: String, Codable, Hashable {
|
||||
public enum HostMode: String, Codable {
|
||||
case onionViaSocks
|
||||
case onionHost = "onion"
|
||||
case publicHost = "public"
|
||||
}
|
||||
|
||||
public enum SocksMode: String, Codable, Hashable {
|
||||
public enum SocksMode: String, Codable {
|
||||
case always = "always"
|
||||
case onion = "onion"
|
||||
}
|
||||
|
||||
public enum SMPProxyMode: String, Codable, Hashable, SelectableItem {
|
||||
public enum SMPProxyMode: String, Codable, SelectableItem {
|
||||
case always = "always"
|
||||
case unknown = "unknown"
|
||||
case unprotected = "unprotected"
|
||||
@@ -1400,7 +1401,7 @@ public enum SMPProxyMode: String, Codable, Hashable, SelectableItem {
|
||||
public static let values: [SMPProxyMode] = [.always, .unknown, .unprotected, .never]
|
||||
}
|
||||
|
||||
public enum SMPProxyFallback: String, Codable, Hashable, SelectableItem {
|
||||
public enum SMPProxyFallback: String, Codable, SelectableItem {
|
||||
case allow = "allow"
|
||||
case allowProtected = "allowProtected"
|
||||
case prohibit = "prohibit"
|
||||
@@ -1418,7 +1419,7 @@ public enum SMPProxyFallback: String, Codable, Hashable, SelectableItem {
|
||||
public static let values: [SMPProxyFallback] = [.allow, .allowProtected, .prohibit]
|
||||
}
|
||||
|
||||
public enum OnionHosts: String, Identifiable, Hashable {
|
||||
public enum OnionHosts: String, Identifiable {
|
||||
case no
|
||||
case prefer
|
||||
case require
|
||||
@@ -1452,7 +1453,7 @@ public enum OnionHosts: String, Identifiable, Hashable {
|
||||
public static let values: [OnionHosts] = [.no, .prefer, .require]
|
||||
}
|
||||
|
||||
public enum TransportSessionMode: String, Codable, Identifiable, Hashable {
|
||||
public enum TransportSessionMode: String, Codable, Identifiable {
|
||||
case user
|
||||
case entity
|
||||
|
||||
@@ -1468,7 +1469,7 @@ public enum TransportSessionMode: String, Codable, Identifiable, Hashable {
|
||||
public static let values: [TransportSessionMode] = [.user, .entity]
|
||||
}
|
||||
|
||||
public struct KeepAliveOpts: Codable, Equatable, Hashable {
|
||||
public struct KeepAliveOpts: Codable, Equatable {
|
||||
public var keepIdle: Int // seconds
|
||||
public var keepIntvl: Int // seconds
|
||||
public var keepCnt: Int // times
|
||||
@@ -1476,7 +1477,64 @@ public struct KeepAliveOpts: Codable, Equatable, Hashable {
|
||||
public static let defaults: KeepAliveOpts = KeepAliveOpts(keepIdle: 30, keepIntvl: 15, keepCnt: 4)
|
||||
}
|
||||
|
||||
public enum NetworkStatus: Decodable, Equatable, Hashable {
|
||||
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
|
||||
case disconnected
|
||||
@@ -1514,7 +1572,7 @@ public enum NetworkStatus: Decodable, Equatable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct ConnNetworkStatus: Decodable, Hashable {
|
||||
public struct ConnNetworkStatus: Decodable {
|
||||
public var agentConnId: String
|
||||
public var networkStatus: NetworkStatus
|
||||
}
|
||||
@@ -1539,7 +1597,7 @@ public enum MsgFilter: String, Codable, Hashable {
|
||||
case mentions
|
||||
}
|
||||
|
||||
public struct UserMsgReceiptSettings: Codable, Hashable {
|
||||
public struct UserMsgReceiptSettings: Codable {
|
||||
public var enable: Bool
|
||||
public var clearOverrides: Bool
|
||||
|
||||
@@ -1588,7 +1646,7 @@ public enum SndSwitchStatus: String, Codable, Hashable {
|
||||
case sendingQTEST = "sending_qtest"
|
||||
}
|
||||
|
||||
public enum QueueDirection: String, Decodable, Hashable {
|
||||
public enum QueueDirection: String, Decodable {
|
||||
case rcv
|
||||
case snd
|
||||
}
|
||||
@@ -1643,12 +1701,12 @@ public struct AutoAccept: Codable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public protocol SelectableItem: Hashable, Identifiable {
|
||||
public protocol SelectableItem: Identifiable, Equatable {
|
||||
var label: LocalizedStringKey { get }
|
||||
static var values: [Self] { get }
|
||||
}
|
||||
|
||||
public struct DeviceToken: Decodable, Hashable {
|
||||
public struct DeviceToken: Decodable {
|
||||
var pushProvider: PushProvider
|
||||
var token: String
|
||||
|
||||
@@ -1662,12 +1720,12 @@ public struct DeviceToken: Decodable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum PushEnvironment: String, Hashable {
|
||||
public enum PushEnvironment: String {
|
||||
case development
|
||||
case production
|
||||
}
|
||||
|
||||
public enum PushProvider: String, Decodable, Hashable {
|
||||
public enum PushProvider: String, Decodable {
|
||||
case apns_dev
|
||||
case apns_prod
|
||||
|
||||
@@ -1681,7 +1739,7 @@ public enum PushProvider: String, Decodable, Hashable {
|
||||
|
||||
// This notification mode is for app core, UI uses AppNotificationsMode.off to mean completely disable,
|
||||
// and .local for periodic background checks
|
||||
public enum NotificationsMode: String, Decodable, SelectableItem, Hashable {
|
||||
public enum NotificationsMode: String, Decodable, SelectableItem {
|
||||
case off = "OFF"
|
||||
case periodic = "PERIODIC"
|
||||
case instant = "INSTANT"
|
||||
@@ -1699,7 +1757,7 @@ public enum NotificationsMode: String, Decodable, SelectableItem, Hashable {
|
||||
public static var values: [NotificationsMode] = [.instant, .periodic, .off]
|
||||
}
|
||||
|
||||
public enum NotificationPreviewMode: String, SelectableItem, Codable, Hashable {
|
||||
public enum NotificationPreviewMode: String, SelectableItem, Codable {
|
||||
case hidden
|
||||
case contact
|
||||
case message
|
||||
@@ -1717,7 +1775,7 @@ public enum NotificationPreviewMode: String, SelectableItem, Codable, Hashable {
|
||||
public static var values: [NotificationPreviewMode] = [.message, .contact, .hidden]
|
||||
}
|
||||
|
||||
public struct RemoteCtrlInfo: Decodable, Hashable {
|
||||
public struct RemoteCtrlInfo: Decodable {
|
||||
public var remoteCtrlId: Int64
|
||||
public var ctrlDeviceName: String
|
||||
public var sessionState: RemoteCtrlSessionState?
|
||||
@@ -1727,7 +1785,7 @@ public struct RemoteCtrlInfo: Decodable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum RemoteCtrlSessionState: Decodable, Hashable {
|
||||
public enum RemoteCtrlSessionState: Decodable {
|
||||
case starting
|
||||
case searching
|
||||
case connecting
|
||||
@@ -1742,17 +1800,17 @@ public enum RemoteCtrlStopReason: Decodable {
|
||||
case disconnected
|
||||
}
|
||||
|
||||
public struct CtrlAppInfo: Decodable, Hashable {
|
||||
public struct CtrlAppInfo: Decodable {
|
||||
public var appVersionRange: AppVersionRange
|
||||
public var deviceName: String
|
||||
}
|
||||
|
||||
public struct AppVersionRange: Decodable, Hashable {
|
||||
public struct AppVersionRange: Decodable {
|
||||
public var minVersion: String
|
||||
public var maxVersion: String
|
||||
}
|
||||
|
||||
public struct CoreVersionInfo: Decodable, Hashable {
|
||||
public struct CoreVersionInfo: Decodable {
|
||||
public var version: String
|
||||
public var simplexmqVersion: String
|
||||
public var simplexmqCommit: String
|
||||
@@ -2090,20 +2148,22 @@ public enum RemoteCtrlError: Decodable, Hashable {
|
||||
case protocolError
|
||||
}
|
||||
|
||||
public struct MigrationFileLinkData: Codable, Hashable {
|
||||
public struct MigrationFileLinkData: Codable {
|
||||
let networkConfig: NetworkConfig?
|
||||
|
||||
public init(networkConfig: NetworkConfig) {
|
||||
self.networkConfig = networkConfig
|
||||
}
|
||||
|
||||
public struct NetworkConfig: Codable, Hashable {
|
||||
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, Hashable {
|
||||
return if let hostMode, let requiredHostMode {
|
||||
NetworkConfig(
|
||||
socksProxy: nil,
|
||||
networkProxy: nil,
|
||||
hostMode: hostMode == .onionViaSocks ? .onionHost : hostMode,
|
||||
requiredHostMode: requiredHostMode
|
||||
)
|
||||
@@ -2129,8 +2190,9 @@ public struct MigrationFileLinkData: Codable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct AppSettings: Codable, Equatable, Hashable {
|
||||
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, Hashable {
|
||||
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, Hashable {
|
||||
public static var defaults: AppSettings {
|
||||
AppSettings (
|
||||
networkConfig: NetCfg.defaults,
|
||||
networkProxy: NetworkProxy.def,
|
||||
privacyEncryptLocalFiles: true,
|
||||
privacyAskToApproveRelays: true,
|
||||
privacyAcceptImages: true,
|
||||
@@ -2224,7 +2288,7 @@ public struct AppSettings: Codable, Equatable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum AppSettingsNotificationMode: String, Codable, Hashable {
|
||||
public enum AppSettingsNotificationMode: String, Codable {
|
||||
case off
|
||||
case periodic
|
||||
case instant
|
||||
@@ -2252,13 +2316,13 @@ public enum AppSettingsNotificationMode: String, Codable, Hashable {
|
||||
// case message
|
||||
//}
|
||||
|
||||
public enum AppSettingsLockScreenCalls: String, Codable, Hashable {
|
||||
public enum AppSettingsLockScreenCalls: String, Codable {
|
||||
case disable
|
||||
case show
|
||||
case accept
|
||||
}
|
||||
|
||||
public struct UserNetworkInfo: Codable, Equatable, Hashable {
|
||||
public struct UserNetworkInfo: Codable, Equatable {
|
||||
public let networkType: UserNetworkType
|
||||
public let online: Bool
|
||||
|
||||
@@ -2268,7 +2332,7 @@ public struct UserNetworkInfo: Codable, Equatable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum UserNetworkType: String, Codable, Hashable {
|
||||
public enum UserNetworkType: String, Codable {
|
||||
case none
|
||||
case cellular
|
||||
case wifi
|
||||
@@ -2286,7 +2350,7 @@ public enum UserNetworkType: String, Codable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct RcvMsgInfo: Codable, Hashable {
|
||||
public struct RcvMsgInfo: Codable {
|
||||
var msgId: Int64
|
||||
var msgDeliveryId: Int64
|
||||
var msgDeliveryStatus: String
|
||||
@@ -2294,7 +2358,16 @@ public struct RcvMsgInfo: Codable, Hashable {
|
||||
var agentMsgMeta: String
|
||||
}
|
||||
|
||||
public struct QueueInfo: Codable, Hashable {
|
||||
public struct ServerQueueInfo: Codable {
|
||||
var server: String
|
||||
var rcvId: String
|
||||
var sndId: String
|
||||
var ntfId: String?
|
||||
var status: String
|
||||
var info: QueueInfo
|
||||
}
|
||||
|
||||
public struct QueueInfo: Codable {
|
||||
var qiSnd: Bool
|
||||
var qiNtf: Bool
|
||||
var qiSub: QSub?
|
||||
@@ -2302,25 +2375,25 @@ public struct QueueInfo: Codable, Hashable {
|
||||
var qiMsg: MsgInfo?
|
||||
}
|
||||
|
||||
public struct QSub: Codable, Hashable {
|
||||
public struct QSub: Codable {
|
||||
var qSubThread: QSubThread
|
||||
var qDelivered: String?
|
||||
}
|
||||
|
||||
public enum QSubThread: String, Codable, Hashable {
|
||||
public enum QSubThread: String, Codable {
|
||||
case noSub
|
||||
case subPending
|
||||
case subThread
|
||||
case prohibitSub
|
||||
}
|
||||
|
||||
public struct MsgInfo: Codable, Hashable {
|
||||
public struct MsgInfo: Codable {
|
||||
var msgId: String
|
||||
var msgTs: Date
|
||||
var msgType: MsgType
|
||||
}
|
||||
|
||||
public enum MsgType: String, Codable, Hashable {
|
||||
public enum MsgType: String, Codable {
|
||||
case message
|
||||
case quota
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1500,6 +1500,12 @@ public struct ChatData: Decodable, Identifiable, Hashable, ChatLike {
|
||||
|
||||
public var id: ChatId { get { chatInfo.id } }
|
||||
|
||||
public init(chatInfo: ChatInfo, chatItems: [ChatItem], chatStats: ChatStats = ChatStats()) {
|
||||
self.chatInfo = chatInfo
|
||||
self.chatItems = chatItems
|
||||
self.chatStats = chatStats
|
||||
}
|
||||
|
||||
public static func invalidJSON(_ json: String) -> ChatData {
|
||||
ChatData(
|
||||
chatInfo: .invalidJSON(json: json),
|
||||
|
||||
@@ -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
|
||||
@@ -506,7 +506,7 @@
|
||||
"Allow to send files and media." = "Se permite enviar archivos y multimedia.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Allow to send SimpleX links." = "Permitir enviar enlaces SimpleX.";
|
||||
"Allow to send SimpleX links." = "Se permite enviar enlaces SimpleX.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Allow to send voice messages." = "Permites enviar mensajes de voz.";
|
||||
@@ -1606,7 +1606,7 @@
|
||||
"Do it later" = "Hacer más tarde";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Do not send history to new members." = "No enviar historial a miembros nuevos.";
|
||||
"Do not send history to new members." = "No se envía el historial a los miembros nuevos.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Do NOT send messages directly, even if your or destination server does not support private routing." = "NO enviar mensajes directamente incluso si tu servidor o el de destino no soportan enrutamiento privado.";
|
||||
@@ -3415,7 +3415,7 @@
|
||||
"Prohibit sending files and media." = "No permitir el envío de archivos y multimedia.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Prohibit sending SimpleX links." = "No permitir el envío de enlaces SimpleX.";
|
||||
"Prohibit sending SimpleX links." = "No se permite enviar enlaces SimpleX.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Prohibit sending voice messages." = "No se permiten mensajes de voz.";
|
||||
@@ -3917,7 +3917,7 @@
|
||||
"Send them from gallery or custom keyboards." = "Envíalos desde la galería o desde teclados personalizados.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Send up to 100 last messages to new members." = "Enviar hasta 100 últimos mensajes a los miembros nuevos.";
|
||||
"Send up to 100 last messages to new members." = "Se envían hasta 100 mensajes más recientes a los miembros nuevos.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Sender cancelled file transfer." = "El remitente ha cancelado la transferencia de archivos.";
|
||||
@@ -4979,7 +4979,7 @@
|
||||
"You can accept calls from lock screen, without device and app authentication." = "Puede aceptar llamadas desde la pantalla de bloqueo, sin autenticación de dispositivos y aplicaciones.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can change it in Appearance settings." = "Puede cambiarlo desde el menú Apariencia.";
|
||||
"You can change it in Appearance settings." = "Puedes cambiar la posición de la barra desde el menú Apariencia.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can create it later" = "Puedes crearla más tarde";
|
||||
|
||||
@@ -659,10 +659,10 @@
|
||||
"Bad desktop address" = "Hibás számítógép cím";
|
||||
|
||||
/* integrity error chat item */
|
||||
"bad message hash" = "téves üzenet hash";
|
||||
"bad message hash" = "hibás az üzenet ellenőrzőösszege";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Bad message hash" = "Téves üzenet hash";
|
||||
"Bad message hash" = "Hibás az üzenet ellenőrzőösszege";
|
||||
|
||||
/* integrity error chat item */
|
||||
"bad message ID" = "téves üzenet ID";
|
||||
@@ -1432,7 +1432,7 @@
|
||||
"Delete link?" = "Hivatkozás törlése?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete member message?" = "Csoporttag üzenet törlése?";
|
||||
"Delete member message?" = "Csoporttag üzenetének törlése?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete message?" = "Üzenet törlése?";
|
||||
@@ -1495,7 +1495,7 @@
|
||||
"Delivery receipts are disabled!" = "A kézbesítési jelentések ki vannak kapcsolva!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delivery receipts!" = "Kézbesítési igazolások!";
|
||||
"Delivery receipts!" = "Üzenet kézbesítési jelentések!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Description" = "Leírás";
|
||||
@@ -1828,10 +1828,10 @@
|
||||
"Enter this device name…" = "Eszköznév megadása…";
|
||||
|
||||
/* placeholder */
|
||||
"Enter welcome message…" = "Üdvözlő üzenetet megadása…";
|
||||
"Enter welcome message…" = "Üdvözlő üzenet megadása…";
|
||||
|
||||
/* placeholder */
|
||||
"Enter welcome message… (optional)" = "Üdvözlő üzenetet megadása… (opcionális)";
|
||||
"Enter welcome message… (optional)" = "Üdvözlő üzenet megadása… (opcionális)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Enter your name…" = "Adjon meg egy nevet…";
|
||||
@@ -2783,7 +2783,7 @@
|
||||
"Message delivery error" = "Üzenetkézbesítési hiba";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Message delivery receipts!" = "Üzenetkézbesítési bizonylatok!";
|
||||
"Message delivery receipts!" = "Üzenet kézbesítési jelentések!";
|
||||
|
||||
/* item status text */
|
||||
"Message delivery warning" = "Üzenet kézbesítési figyelmeztetés";
|
||||
@@ -2813,7 +2813,7 @@
|
||||
"message received" = "üzenet érkezett";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Message reception" = "Üzenetjelentés";
|
||||
"Message reception" = "Üzenet kézbesítési jelentés";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Message servers" = "Üzenetkiszolgálók";
|
||||
@@ -3605,7 +3605,7 @@
|
||||
"removed" = "eltávolítva";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"removed %@" = "%@ eltávolítva";
|
||||
"removed %@" = "eltávolította őt: %@";
|
||||
|
||||
/* profile update event chat item */
|
||||
"removed contact address" = "törölt kapcsolattartási cím";
|
||||
@@ -4373,7 +4373,7 @@
|
||||
"The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "A titkosítás működik, és új titkosítási egyezményre nincs szükség. Ez kapcsolati hibákat eredményezhet!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"The hash of the previous message is different." = "Az előző üzenet hash-e más.";
|
||||
"The hash of the previous message is different." = "Az előző üzenet ellenőrzőösszege különbözik.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised." = "A következő üzenet azonosítója hibás (kisebb vagy egyenlő az előzővel).\nEz valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő.";
|
||||
|
||||
@@ -1089,7 +1089,7 @@
|
||||
"Connection" = "Connessione";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connection and servers status." = "Stato di connessione e server.";
|
||||
"Connection and servers status." = "Stato della connessione e dei server.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connection error" = "Errore di connessione";
|
||||
|
||||
@@ -2630,7 +2630,7 @@
|
||||
"Keep" = "Bewaar";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Keep conversation" = "Blijf in gesprek";
|
||||
"Keep conversation" = "Behoud het gesprek";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Keep the app open to use it from desktop" = "Houd de app geopend om deze vanaf de desktop te gebruiken";
|
||||
|
||||
@@ -472,6 +472,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Allow calls only if your contact allows them." = "Zezwalaj na połączenia tylko wtedy, gdy Twój kontakt na to pozwala.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Allow calls?" = "Zezwolić na połączenia?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Allow disappearing messages only if your contact allows it to you." = "Zezwól na znikające wiadomości tylko wtedy, gdy Twój kontakt Ci na to pozwoli.";
|
||||
|
||||
@@ -493,6 +496,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Allow sending disappearing messages." = "Zezwól na wysyłanie znikających wiadomości.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Allow sharing" = "Zezwól na udostępnianie";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Allow to irreversibly delete sent messages. (24 hours)" = "Zezwól na nieodwracalne usunięcie wysłanych wiadomości. (24 godziny)";
|
||||
|
||||
@@ -589,6 +595,12 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Archive and upload" = "Archiwizuj i prześlij";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Archive contacts to chat later." = "Archiwizuj kontakty aby porozmawiać później.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Archived contacts" = "Zarchiwizowane kontakty";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Archiving database" = "Archiwizowanie bazy danych";
|
||||
|
||||
@@ -664,6 +676,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Better messages" = "Lepsze wiadomości";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Better networking" = "Lepsze sieciowanie";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Black" = "Czarny";
|
||||
|
||||
@@ -697,6 +712,12 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Blocked by admin" = "Zablokowany przez admina";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Blur for better privacy." = "Rozmycie dla lepszej prywatności.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Blur media" = "Rozmycie mediów";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"bold" = "pogrubiona";
|
||||
|
||||
@@ -721,6 +742,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Według profilu czatu (domyślnie) lub [według połączenia](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"call" = "zadzwoń";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Call already ended!" = "Połączenie już zakończone!";
|
||||
|
||||
@@ -736,15 +760,27 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Calls" = "Połączenia";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Calls prohibited!" = "Połączenia zakazane!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Camera not available" = "Kamera nie dostępna";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Can't call contact" = "Nie można zadzwonić do kontaktu";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Can't call member" = "Nie można zadzwonić do członka";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Can't invite contact!" = "Nie można zaprosić kontaktu!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Can't invite contacts!" = "Nie można zaprosić kontaktów!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Can't message member" = "Nie można wysłać wiadomości do członka";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Cancel" = "Anuluj";
|
||||
|
||||
@@ -830,6 +866,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Chat database deleted" = "Baza danych czatu usunięta";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Chat database exported" = "Wyeksportowano bazę danych czatu";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Chat database imported" = "Zaimportowano bazę danych czatu";
|
||||
|
||||
@@ -842,6 +881,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." = "Czat został zatrzymany. Jeśli korzystałeś już z tej bazy danych na innym urządzeniu, powinieneś przenieść ją z powrotem przed rozpoczęciem czatu.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Chat list" = "Lista czatów";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Chat migrated!" = "Czat zmigrowany!";
|
||||
|
||||
@@ -893,6 +935,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Clear verification" = "Wyczyść weryfikację";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Color chats with the new themes." = "Koloruj czaty z nowymi motywami.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Color mode" = "Tryb koloru";
|
||||
|
||||
@@ -920,6 +965,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Confirm" = "Potwierdź";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Confirm contact deletion?" = "Potwierdzić usunięcie kontaktu?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Confirm database upgrades" = "Potwierdź aktualizacje bazy danych";
|
||||
|
||||
@@ -959,6 +1007,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"connect to SimpleX Chat developers." = "połącz się z deweloperami SimpleX Chat.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connect to your friends faster." = "Szybciej łącz się ze znajomymi.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connect to yourself?" = "Połączyć się ze sobą?";
|
||||
|
||||
@@ -1025,6 +1076,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Connecting server… (error: %@)" = "Łączenie z serwerem... (błąd: %@)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connecting to contact, please wait or check later!" = "Łączenie z kontaktem, poczekaj lub sprawdź później!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connecting to desktop" = "Łączenie z komputerem";
|
||||
|
||||
@@ -1034,6 +1088,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Connection" = "Połączenie";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connection and servers status." = "Stan połączenia i serwerów.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connection error" = "Błąd połączenia";
|
||||
|
||||
@@ -1043,6 +1100,9 @@
|
||||
/* chat list item title (it should not be shown */
|
||||
"connection established" = "połączenie ustanowione";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connection notifications" = "Powiadomienia o połączeniu";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connection request sent!" = "Prośba o połączenie wysłana!";
|
||||
|
||||
@@ -1070,6 +1130,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Contact already exists" = "Kontakt już istnieje";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Contact deleted!" = "Kontakt usunięty!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"contact has e2e encryption" = "kontakt posiada szyfrowanie e2e";
|
||||
|
||||
@@ -1082,12 +1145,18 @@
|
||||
/* notification */
|
||||
"Contact is connected" = "Kontakt jest połączony";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Contact is deleted." = "Kontakt jest usunięty.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Contact name" = "Nazwa kontaktu";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Contact preferences" = "Preferencje kontaktu";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Contact will be deleted - this cannot be undone!" = "Kontakt zostanie usunięty – nie można tego cofnąć!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Contacts" = "Kontakty";
|
||||
|
||||
@@ -1097,6 +1166,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Continue" = "Kontynuuj";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Conversation deleted!" = "Rozmowa usunięta!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Copy" = "Kopiuj";
|
||||
|
||||
@@ -1281,6 +1353,9 @@
|
||||
swipe action */
|
||||
"Delete" = "Usuń";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete %lld messages of members?" = "Usunąć %lld wiadomości członków?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete %lld messages?" = "Usunąć %lld wiadomości?";
|
||||
|
||||
@@ -1317,6 +1392,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Delete contact" = "Usuń kontakt";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete contact?" = "Usunąć kontakt?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete database" = "Usuń bazę danych";
|
||||
|
||||
@@ -1380,9 +1458,15 @@
|
||||
/* server test step */
|
||||
"Delete queue" = "Usuń kolejkę";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete up to 20 messages at once." = "Usuń do 20 wiadomości na raz.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete user profile?" = "Usunąć profil użytkownika?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete without notification" = "Usuń bez powiadomienia";
|
||||
|
||||
/* deleted chat item */
|
||||
"deleted" = "usunięty";
|
||||
|
||||
@@ -1425,9 +1509,15 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Desktop devices" = "Urządzenia komputerowe";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Destination server address of %@ is incompatible with forwarding server %@ settings." = "Adres serwera docelowego %@ jest niekompatybilny z ustawieniami serwera przekazującego %@.";
|
||||
|
||||
/* snd error text */
|
||||
"Destination server error: %@" = "Błąd docelowego serwera: %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Destination server version of %@ is incompatible with forwarding server %@." = "Wersja serwera docelowego %@ jest niekompatybilna z serwerem przekierowującym %@.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Detailed statistics" = "Szczegółowe statystyki";
|
||||
|
||||
@@ -1437,6 +1527,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Develop" = "Deweloperskie";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Developer options" = "Opcje deweloperskie";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Developer tools" = "Narzędzia deweloperskie";
|
||||
|
||||
@@ -1476,6 +1569,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"disabled" = "wyłączony";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Disabled" = "Wyłączony";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Disappearing message" = "Znikająca wiadomość";
|
||||
|
||||
@@ -1623,6 +1719,9 @@
|
||||
/* enabled status */
|
||||
"enabled" = "włączone";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Enabled" = "Włączony";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Enabled for" = "Włączony dla";
|
||||
|
||||
@@ -1764,6 +1863,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Error changing setting" = "Błąd zmiany ustawienia";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error connecting to forwarding server %@. Please try later." = "Błąd połączenia z serwerem przekierowania %@. Spróbuj ponownie później.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error creating address" = "Błąd tworzenia adresu";
|
||||
|
||||
@@ -2074,6 +2176,15 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Forwarded from" = "Przekazane dalej od";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Serwer przekazujący %@ nie mógł połączyć się z serwerem docelowym %@. Spróbuj ponownie później.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Forwarding server address is incompatible with network settings: %@." = "Adres serwera przekierowującego jest niekompatybilny z ustawieniami sieciowymi: %@.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Forwarding server version is incompatible with network settings: %@." = "Wersja serwera przekierowującego jest niekompatybilna z ustawieniami sieciowymi: %@.";
|
||||
|
||||
/* snd error text */
|
||||
"Forwarding server: %@\nDestination server error: %@" = "Serwer przekazujący: %1$@\nBłąd serwera docelowego: %2$@";
|
||||
|
||||
@@ -2425,6 +2536,9 @@
|
||||
/* group name */
|
||||
"invitation to group %@" = "zaproszenie do grupy %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"invite" = "zaproś";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Invite friends" = "Zaproś znajomych";
|
||||
|
||||
@@ -2470,6 +2584,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Może to nastąpić, gdy:\n1. Wiadomości wygasły w wysyłającym kliencie po 2 dniach lub na serwerze po 30 dniach.\n2. Odszyfrowanie wiadomości nie powiodło się, ponieważ Ty lub Twój kontakt użyliście starej kopii zapasowej bazy danych.\n3. Połączenie zostało skompromitowane.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"It protects your IP address and connections." = "Chroni Twój adres IP i połączenia.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Wygląda na to, że jesteś już połączony przez ten link. Jeśli tak nie jest, wystąpił błąd (%@).";
|
||||
|
||||
@@ -2512,6 +2629,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Keep" = "Zachowaj";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Keep conversation" = "Zachowaj rozmowę";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Keep the app open to use it from desktop" = "Zostaw aplikację otwartą i używaj ją z komputera";
|
||||
|
||||
@@ -2623,6 +2743,12 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Max 30 seconds, received instantly." = "Maksymalnie 30 sekund, odbierane natychmiast.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Media & file servers" = "Serwery mediów i plików";
|
||||
|
||||
/* blur media */
|
||||
"Medium" = "Średni";
|
||||
|
||||
/* member role */
|
||||
"member" = "członek";
|
||||
|
||||
@@ -2650,6 +2776,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Menus" = "Menu";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"message" = "wiadomość";
|
||||
|
||||
/* item status text */
|
||||
"Message delivery error" = "Błąd dostarczenia wiadomości";
|
||||
|
||||
@@ -2686,6 +2815,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Message reception" = "Odebranie wiadomości";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Message servers" = "Serwery wiadomości";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Message source remains private." = "Źródło wiadomości pozostaje prywatne.";
|
||||
|
||||
@@ -2794,6 +2926,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Multiple chat profiles" = "Wiele profili czatu";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"mute" = "wycisz";
|
||||
|
||||
/* swipe action */
|
||||
"Mute" = "Wycisz";
|
||||
|
||||
@@ -2827,6 +2962,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"New chat" = "Nowy czat";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"New chat experience 🎉" = "Nowe możliwości czatu 🎉";
|
||||
|
||||
/* notification */
|
||||
"New contact request" = "Nowa prośba o kontakt";
|
||||
|
||||
@@ -2845,6 +2983,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"New in %@" = "Nowość w %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"New media options" = "Nowe opcje mediów";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"New member role" = "Nowa rola członka";
|
||||
|
||||
@@ -2914,6 +3055,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Not compatible!" = "Nie kompatybilny!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Nothing selected" = "Nic nie jest zaznaczone";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Notifications" = "Powiadomienia";
|
||||
|
||||
@@ -2970,6 +3114,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Tylko urządzenia klienckie przechowują profile użytkowników, kontakty, grupy i wiadomości wysyłane za pomocą **2-warstwowego szyfrowania end-to-end**.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Only delete conversation" = "Usuń tylko rozmowę";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Only group owners can change group preferences." = "Tylko właściciele grup mogą zmieniać preferencje grupy.";
|
||||
|
||||
@@ -3126,6 +3273,12 @@
|
||||
/* No comment provided by engineer. */
|
||||
"PING interval" = "Interwał PINGU";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Play from the chat list." = "Odtwórz z listy czatów.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Please ask your contact to enable calls." = "Poproś kontakt o włącznie połączeń.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Please ask your contact to enable sending voice messages." = "Poproś Twój kontakt o włączenie wysyłania wiadomości głosowych.";
|
||||
|
||||
@@ -3306,6 +3459,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Rate the app" = "Oceń aplikację";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Reachable chat toolbar" = "Osiągalny pasek narzędzi czatu";
|
||||
|
||||
/* chat item menu */
|
||||
"React…" = "Reaguj…";
|
||||
|
||||
@@ -3493,6 +3649,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Reset" = "Resetuj";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Reset all hints" = "Zresetuj wszystkie wskazówki";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Reset all statistics" = "Resetuj wszystkie statystyki";
|
||||
|
||||
@@ -3568,6 +3727,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Save and notify group members" = "Zapisz i powiadom członków grupy";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Save and reconnect" = "Zapisz i połącz ponownie";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Save and update group profile" = "Zapisz i zaktualizuj profil grupowy";
|
||||
|
||||
@@ -3643,6 +3805,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Scan server QR code" = "Zeskanuj kod QR serwera";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"search" = "szukaj";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Search" = "Szukaj";
|
||||
|
||||
@@ -3682,6 +3847,9 @@
|
||||
/* chat item action */
|
||||
"Select" = "Wybierz";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Selected %lld" = "Zaznaczono %lld";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Selected chat preferences prohibit this message." = "Wybrane preferencje czatu zabraniają tej wiadomości.";
|
||||
|
||||
@@ -3724,6 +3892,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Send live message" = "Wyślij wiadomość na żywo";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Send message to enable calls." = "Wyślij wiadomość aby włączyć połączenia.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Send messages directly when IP address is protected and your or destination server does not support private routing." = "Wysyłaj wiadomości bezpośrednio, gdy adres IP jest chroniony i Twój lub docelowy serwer nie obsługuje prywatnego trasowania.";
|
||||
|
||||
@@ -3904,12 +4075,18 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Share address with contacts?" = "Udostępnić adres kontaktom?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Share from other apps." = "Udostępnij z innych aplikacji.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Share link" = "Udostępnij link";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Share this 1-time invite link" = "Udostępnij ten jednorazowy link";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Share to SimpleX" = "Udostępnij do SimpleX";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Share with contacts" = "Udostępnij kontaktom";
|
||||
|
||||
@@ -4003,9 +4180,18 @@
|
||||
/* No comment provided by engineer. */
|
||||
"SMP server" = "Serwer SMP";
|
||||
|
||||
/* blur media */
|
||||
"Soft" = "Łagodny";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Some file(s) were not exported:" = "Niektóre plik(i) nie zostały wyeksportowane:";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Some non-fatal errors occurred during import - you may see Chat console for more details." = "Podczas importu wystąpiły niekrytyczne błędy - więcej szczegółów można znaleźć w konsoli czatu.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Some non-fatal errors occurred during import:" = "Podczas importu wystąpiły niekrytyczne błędy:";
|
||||
|
||||
/* notification title */
|
||||
"Somebody" = "Ktoś";
|
||||
|
||||
@@ -4072,6 +4258,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"strike" = "strajk";
|
||||
|
||||
/* blur media */
|
||||
"Strong" = "Silne";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Submit" = "Zatwierdź";
|
||||
|
||||
@@ -4117,6 +4306,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Tap to scan" = "Dotknij, aby zeskanować";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"TCP connection" = "Połączenie TCP";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"TCP connection timeout" = "Limit czasu połączenia TCP";
|
||||
|
||||
@@ -4192,6 +4384,12 @@
|
||||
/* No comment provided by engineer. */
|
||||
"The message will be marked as moderated for all members." = "Wiadomość zostanie oznaczona jako moderowana dla wszystkich członków.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"The messages will be deleted for all members." = "Wiadomości zostaną usunięte dla wszystkich członków.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"The messages will be marked as moderated for all members." = "Wiadomości zostaną oznaczone jako moderowane dla wszystkich członków.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"The next generation of private messaging" = "Następna generacja prywatnych wiadomości";
|
||||
|
||||
@@ -4303,9 +4501,15 @@
|
||||
/* No comment provided by engineer. */
|
||||
"To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Aby zweryfikować szyfrowanie end-to-end z Twoim kontaktem porównaj (lub zeskanuj) kod na waszych urządzeniach.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Toggle chat list:" = "Przełącz listę czatów:";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Toggle incognito when connecting." = "Przełącz incognito przy połączeniu.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Toolbar opacity" = "Nieprzezroczystość paska narzędzi";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Total" = "Łącznie";
|
||||
|
||||
@@ -4408,6 +4612,9 @@
|
||||
/* authentication reason */
|
||||
"Unlock app" = "Odblokuj aplikację";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"unmute" = "wyłącz wyciszenie";
|
||||
|
||||
/* swipe action */
|
||||
"Unmute" = "Wyłącz wyciszenie";
|
||||
|
||||
@@ -4429,6 +4636,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Update network settings?" = "Zaktualizować ustawienia sieci?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Update settings?" = "Zaktualizować ustawienia?";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"updated group profile" = "zaktualizowano profil grupy";
|
||||
|
||||
@@ -4498,6 +4708,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Use the app while in the call." = "Używaj aplikacji podczas połączenia.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Use the app with one hand." = "Korzystaj z aplikacji jedną ręką.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"User profile" = "Profil użytkownika";
|
||||
|
||||
@@ -4552,6 +4765,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Via secure quantum resistant protocol." = "Dzięki bezpiecznemu protokołowi odpornego kwantowo.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"video" = "wideo";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Video call" = "Połączenie wideo";
|
||||
|
||||
@@ -4762,6 +4978,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"You can accept calls from lock screen, without device and app authentication." = "Możesz przyjmować połączenia z ekranu blokady, bez uwierzytelniania urządzenia i aplikacji.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can change it in Appearance settings." = "Możesz to zmienić w ustawieniach wyglądu.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can create it later" = "Możesz go utworzyć później";
|
||||
|
||||
@@ -4783,6 +5002,9 @@
|
||||
/* notification body */
|
||||
"You can now chat with %@" = "Możesz teraz wysyłać wiadomości do %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can send messages to %@ from Archived contacts." = "Możesz wysyłać wiadomości do %@ ze zarchiwizowanych kontaktów.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can set lock screen notification preview via settings." = "Podgląd powiadomień na ekranie blokady można ustawić w ustawieniach.";
|
||||
|
||||
@@ -4798,6 +5020,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"You can start chat via app Settings / Database or by restarting the app" = "Możesz rozpocząć czat poprzez Ustawienia aplikacji / Baza danych lub poprzez ponowne uruchomienie aplikacji";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can still view conversation with %@ in the list of chats." = "Nadal możesz przeglądać rozmowę z %@ na liście czatów.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can turn on SimpleX Lock via Settings." = "Możesz włączyć blokadę SimpleX poprzez Ustawienia.";
|
||||
|
||||
@@ -4849,9 +5074,18 @@
|
||||
/* snd group event chat item */
|
||||
"you left" = "wyszedłeś";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You may migrate the exported database." = "Możesz zmigrować wyeksportowaną bazy danych.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You may save the exported archive." = "Możesz zapisać wyeksportowane archiwum.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "Musisz używać najnowszej wersji bazy danych czatu TYLKO na jednym urządzeniu, w przeciwnym razie możesz przestać otrzymywać wiadomości od niektórych kontaktów.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You need to allow your contact to call to be able to call them." = "Aby móc dzwonić, musisz zezwolić kontaktowi na połączenia.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You need to allow your contact to send voice messages to be able to send them." = "Musisz zezwolić Twojemu kontaktowi na wysyłanie wiadomości głosowych, aby móc je wysyłać.";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
}
|
||||
|
||||
+3
@@ -52,6 +52,9 @@ actual fun windowOrientation(): WindowOrientation = when (mainActivity.get()?.re
|
||||
@Composable
|
||||
actual fun windowWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp
|
||||
|
||||
@Composable
|
||||
actual fun windowHeight(): Dp = LocalConfiguration.current.screenHeightDp.dp
|
||||
|
||||
actual fun desktopExpandWindowToWidth(width: Dp) {}
|
||||
|
||||
actual fun isRtl(text: CharSequence): Boolean = BidiFormatter.getInstance().isRtl(text)
|
||||
|
||||
+56
-6
@@ -4,14 +4,21 @@ import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.gestures.FlingBehavior
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@Composable
|
||||
actual fun LazyColumnWithScrollBar(
|
||||
modifier: Modifier,
|
||||
state: LazyListState,
|
||||
state: LazyListState?,
|
||||
contentPadding: PaddingValues,
|
||||
reverseLayout: Boolean,
|
||||
verticalArrangement: Arrangement.Vertical,
|
||||
@@ -20,7 +27,24 @@ actual fun LazyColumnWithScrollBar(
|
||||
userScrollEnabled: Boolean,
|
||||
content: LazyListScope.() -> Unit
|
||||
) {
|
||||
LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content)
|
||||
val state = state ?: LocalAppBarHandler.current?.listState ?: rememberLazyListState()
|
||||
val connection = LocalAppBarHandler.current?.connection
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { state.firstVisibleItemScrollOffset }
|
||||
.filter { state.firstVisibleItemIndex == 0 }
|
||||
.collect { scrollPosition ->
|
||||
val offset = connection?.appBarOffset
|
||||
if (offset != null && (offset + scrollPosition).absoluteValue > 1) {
|
||||
connection.appBarOffset = -scrollPosition.toFloat()
|
||||
// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (connection != null) {
|
||||
LazyColumn(modifier.nestedScroll(connection), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content)
|
||||
} else {
|
||||
LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -28,8 +52,34 @@ actual fun ColumnWithScrollBar(
|
||||
modifier: Modifier,
|
||||
verticalArrangement: Arrangement.Vertical,
|
||||
horizontalAlignment: Alignment.Horizontal,
|
||||
state: ScrollState,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
state: ScrollState?,
|
||||
maxIntrinsicSize: Boolean,
|
||||
content: @Composable() (ColumnScope.() -> Unit)
|
||||
) {
|
||||
Column(modifier.verticalScroll(rememberScrollState()), verticalArrangement, horizontalAlignment, content)
|
||||
val state = state ?: LocalAppBarHandler.current?.scrollState ?: rememberScrollState()
|
||||
val connection = LocalAppBarHandler.current?.connection
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { state.value }
|
||||
.collect { scrollPosition ->
|
||||
val offset = connection?.appBarOffset
|
||||
if (offset != null && (offset + scrollPosition).absoluteValue > 1) {
|
||||
connection.appBarOffset = -scrollPosition.toFloat()
|
||||
// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (connection != null) {
|
||||
Column(
|
||||
if (maxIntrinsicSize) {
|
||||
modifier.nestedScroll(connection).verticalScroll(state).height(IntrinsicSize.Max)
|
||||
} else {
|
||||
modifier.nestedScroll(connection).verticalScroll(state)
|
||||
}, verticalArrangement, horizontalAlignment, content)
|
||||
} else {
|
||||
Column(if (maxIntrinsicSize) {
|
||||
modifier.verticalScroll(state).height(IntrinsicSize.Max)
|
||||
} else {
|
||||
modifier.verticalScroll(state)
|
||||
}, verticalArrangement, horizontalAlignment, content)
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -457,7 +457,7 @@ private fun DisabledBackgroundCallsButton() {
|
||||
) {
|
||||
Text(stringResource(MR.strings.system_restricted_background_in_call_title), color = WarningOrange)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
IconButton(onClick = { show = false }, Modifier.size(24.dp)) {
|
||||
IconButton(onClick = { show = false }, Modifier.size(22.dp)) {
|
||||
Icon(painterResource(MR.images.ic_close), null, tint = WarningOrange)
|
||||
}
|
||||
}
|
||||
@@ -539,7 +539,7 @@ fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Uni
|
||||
Icon(
|
||||
painterResource(MR.images.ic_call_500),
|
||||
stringResource(MR.strings.permissions_record_audio),
|
||||
Modifier.size(24.dp),
|
||||
Modifier.size(22.dp),
|
||||
tint = Color(0xFFFFFFD8)
|
||||
)
|
||||
}
|
||||
@@ -547,7 +547,7 @@ fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Uni
|
||||
Icon(
|
||||
painterResource(MR.images.ic_videocam),
|
||||
stringResource(MR.strings.permissions_camera),
|
||||
Modifier.size(24.dp),
|
||||
Modifier.size(22.dp),
|
||||
tint = Color(0xFFFFFFD8)
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-9
@@ -13,12 +13,13 @@ import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.MaterialTheme.colors
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
@@ -30,7 +31,6 @@ import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.helpers.APPLICATION_ID
|
||||
import chat.simplex.common.helpers.saveAppLocale
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
@@ -79,7 +79,7 @@ fun AppearanceScope.AppearanceLayout(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.appearance_settings))
|
||||
SectionView(stringResource(MR.strings.settings_section_title_interface), padding = PaddingValues()) {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_interface), contentPadding = PaddingValues()) {
|
||||
val context = LocalContext.current
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
// SectionItemWithValue(
|
||||
@@ -112,15 +112,15 @@ fun AppearanceScope.AppearanceLayout(
|
||||
}
|
||||
}
|
||||
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
SectionDividerSpaced()
|
||||
ThemesSection(systemDarkTheme)
|
||||
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
SectionDividerSpaced()
|
||||
ProfileImageSection()
|
||||
|
||||
SectionDividerSpaced()
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_icon), padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_icon), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
|
||||
LazyRow {
|
||||
items(AppIcon.values().size, { index -> AppIcon.values()[index] }) { index ->
|
||||
val item = AppIcon.values()[index]
|
||||
@@ -129,7 +129,8 @@ fun AppearanceScope.AppearanceLayout(
|
||||
contentDescription = "",
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier
|
||||
.shadow(if (item == icon.value) 1.dp else 0.dp, ambientColor = colors.secondaryVariant)
|
||||
.border(1.dp, color = if (item == icon.value) colors.secondaryVariant else Color.Transparent, RoundedCornerShape(percent = 22))
|
||||
.clip(RoundedCornerShape(percent = 22))
|
||||
.size(70.dp)
|
||||
.clickable { changeIcon(item) }
|
||||
.padding(10.dp)
|
||||
@@ -143,7 +144,7 @@ fun AppearanceScope.AppearanceLayout(
|
||||
}
|
||||
}
|
||||
|
||||
SectionDividerSpaced(maxBottomPadding = true)
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
FontScaleSection()
|
||||
|
||||
SectionBottomSpacer()
|
||||
|
||||
+4
-5
@@ -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.*
|
||||
@@ -19,9 +18,9 @@ actual fun SettingsSectionApp(
|
||||
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
|
||||
) {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_app)) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_restart_alt), stringResource(MR.strings.settings_restart_app), ::restartApp, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), { shutdownAppAlert(::shutdownApp) }, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) }, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_restart_alt), stringResource(MR.strings.settings_restart_app), ::restartApp)
|
||||
SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), { shutdownAppAlert(::shutdownApp) })
|
||||
SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) })
|
||||
AppVersionItem(showVersion)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView
|
||||
import chat.simplex.common.model.*
|
||||
@@ -50,6 +51,7 @@ data class SettingsViewState(
|
||||
|
||||
@Composable
|
||||
fun AppScreen() {
|
||||
AppBarHandler.appBarMaxHeightPx = with(LocalDensity.current) { AppBarHeight.roundToPx() }
|
||||
SimpleXTheme {
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||
@@ -72,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
|
||||
@@ -209,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()
|
||||
}
|
||||
|
||||
+11
-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
|
||||
}
|
||||
@@ -985,6 +986,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val fullName get() = contact.fullName
|
||||
override val image get() = contact.image
|
||||
override val localAlias: String get() = contact.localAlias
|
||||
override fun anyNameContains(searchAnyCase: String): Boolean = contact.anyNameContains(searchAnyCase)
|
||||
|
||||
companion object {
|
||||
val sampleData = Direct(Contact.sampleData)
|
||||
@@ -1219,6 +1221,12 @@ data class Contact(
|
||||
override val localAlias get() = profile.localAlias
|
||||
val verified get() = activeConn?.connectionCode != null
|
||||
|
||||
override fun anyNameContains(searchAnyCase: String): Boolean {
|
||||
val s = searchAnyCase.trim().lowercase()
|
||||
return profile.chatViewName.lowercase().contains(s) || profile.displayName.lowercase().contains(s) || profile.fullName.lowercase().contains(s)
|
||||
}
|
||||
|
||||
|
||||
val directOrUsed: Boolean get() =
|
||||
if (activeConn != null) {
|
||||
(activeConn.connLevel == 0 && !activeConn.viaGroupLink) || contactUsed
|
||||
|
||||
+84
-24
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1030,14 +1047,14 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiContactQueueInfo(rh: Long?, contactId: Long): Pair<RcvMsgInfo?, QueueInfo>? {
|
||||
suspend fun apiContactQueueInfo(rh: Long?, contactId: Long): Pair<RcvMsgInfo?, ServerQueueInfo>? {
|
||||
val r = sendCmd(rh, CC.APIContactQueueInfo(contactId))
|
||||
if (r is CR.QueueInfoR) return Pair(r.rcvMsgInfo, r.queueInfo)
|
||||
apiErrorAlert("apiContactQueueInfo", generalGetString(MR.strings.error), r)
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiGroupMemberQueueInfo(rh: Long?, groupId: Long, groupMemberId: Long): Pair<RcvMsgInfo?, QueueInfo>? {
|
||||
suspend fun apiGroupMemberQueueInfo(rh: Long?, groupId: Long, groupMemberId: Long): Pair<RcvMsgInfo?, ServerQueueInfo>? {
|
||||
val r = sendCmd(rh, CC.APIGroupMemberQueueInfo(groupId, groupMemberId))
|
||||
if (r is CR.QueueInfoR) return Pair(r.rcvMsgInfo, r.queueInfo)
|
||||
apiErrorAlert("apiGroupMemberQueueInfo", generalGetString(MR.strings.error), r)
|
||||
@@ -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
|
||||
}
|
||||
@@ -4734,7 +4775,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("networkConfig") class NetworkConfig(val networkConfig: NetCfg): CR()
|
||||
@Serializable @SerialName("contactInfo") class ContactInfo(val user: UserRef, val contact: Contact, val connectionStats_: ConnectionStats? = null, val customUserProfile: Profile? = null): CR()
|
||||
@Serializable @SerialName("groupMemberInfo") class GroupMemberInfo(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats_: ConnectionStats? = null): CR()
|
||||
@Serializable @SerialName("queueInfo") class QueueInfoR(val user: UserRef, val rcvMsgInfo: RcvMsgInfo?, val queueInfo: QueueInfo): CR()
|
||||
@Serializable @SerialName("queueInfo") class QueueInfoR(val user: UserRef, val rcvMsgInfo: RcvMsgInfo?, val queueInfo: ServerQueueInfo): CR()
|
||||
@Serializable @SerialName("contactSwitchStarted") class ContactSwitchStarted(val user: UserRef, val contact: Contact, val connectionStats: ConnectionStats): CR()
|
||||
@Serializable @SerialName("groupMemberSwitchStarted") class GroupMemberSwitchStarted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR()
|
||||
@Serializable @SerialName("contactSwitchAborted") class ContactSwitchAborted(val user: UserRef, val contact: Contact, val connectionStats: ConnectionStats): CR()
|
||||
@@ -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(),
|
||||
@@ -6409,6 +6459,16 @@ data class RcvMsgInfo (
|
||||
val agentMsgMeta: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ServerQueueInfo (
|
||||
val server: String,
|
||||
val rcvId: String,
|
||||
val sndId: String,
|
||||
val ntfId: String? = null,
|
||||
val status: String,
|
||||
val info: QueueInfo
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class QueueInfo (
|
||||
val qiSnd: Boolean,
|
||||
|
||||
-3
@@ -28,9 +28,6 @@ interface PlatformInterface {
|
||||
fun androidRestartNetworkObserver() {}
|
||||
@Composable fun androidLockPortraitOrientation() {}
|
||||
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
|
||||
@Composable fun desktopScrollBarComponents(): Triple<Animatable<Float, AnimationVector1D>, Modifier, MutableState<Job>> = remember { Triple(Animatable(0f), Modifier, mutableStateOf(Job())) }
|
||||
@Composable fun desktopScrollBar(state: LazyListState, modifier: Modifier, scrollBarAlpha: Animatable<Float, AnimationVector1D>, scrollJob: MutableState<Job>, reversed: Boolean) {}
|
||||
@Composable fun desktopScrollBar(state: ScrollState, modifier: Modifier, scrollBarAlpha: Animatable<Float, AnimationVector1D>, scrollJob: MutableState<Job>, reversed: Boolean) {}
|
||||
@Composable fun desktopShowAppUpdateNotice() {}
|
||||
}
|
||||
/**
|
||||
|
||||
+3
@@ -30,6 +30,9 @@ expect fun windowOrientation(): WindowOrientation
|
||||
@Composable
|
||||
expect fun windowWidth(): Dp
|
||||
|
||||
@Composable
|
||||
expect fun windowHeight(): Dp
|
||||
|
||||
expect fun desktopExpandWindowToWidth(width: Dp)
|
||||
|
||||
expect fun isRtl(text: CharSequence): Boolean
|
||||
|
||||
+4
-2
@@ -13,7 +13,7 @@ import androidx.compose.ui.unit.dp
|
||||
@Composable
|
||||
expect fun LazyColumnWithScrollBar(
|
||||
modifier: Modifier = Modifier,
|
||||
state: LazyListState = rememberLazyListState(),
|
||||
state: LazyListState? = null,
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
reverseLayout: Boolean = false,
|
||||
verticalArrangement: Arrangement.Vertical =
|
||||
@@ -29,6 +29,8 @@ expect fun ColumnWithScrollBar(
|
||||
modifier: Modifier = Modifier,
|
||||
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
|
||||
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||
state: ScrollState = rememberScrollState(),
|
||||
state: ScrollState? = null,
|
||||
// set true when you want to show something in the center with respected .fillMaxSize()
|
||||
maxIntrinsicSize: Boolean = false,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
)
|
||||
|
||||
@@ -607,6 +607,8 @@ val DEFAULT_SPACE_AFTER_ICON = 4.dp
|
||||
val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2
|
||||
val DEFAULT_BOTTOM_PADDING = 48.dp
|
||||
val DEFAULT_BOTTOM_BUTTON_PADDING = 20.dp
|
||||
val DEFAULT_MIN_SECTION_ITEM_HEIGHT = 50.dp
|
||||
val DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL = 15.dp
|
||||
|
||||
val DEFAULT_START_MODAL_WIDTH = 388.dp
|
||||
val DEFAULT_MIN_CENTER_MODAL_WIDTH = 590.dp
|
||||
|
||||
+3
-1
@@ -49,7 +49,9 @@ object ThemeManager {
|
||||
return perUserTheme
|
||||
}
|
||||
val defaultTheme = defaultActiveTheme(appSettingsTheme)
|
||||
return ThemeModeOverride(colors = defaultTheme?.colors ?: ThemeColors(), wallpaper = defaultTheme?.wallpaper)
|
||||
return ThemeModeOverride(colors = defaultTheme?.colors ?: ThemeColors(), wallpaper = defaultTheme?.wallpaper
|
||||
// Situation when user didn't change global theme at all (it is not saved yet). Using defaults
|
||||
?: ThemeWallpaper.from(PresetWallpaper.SCHOOL.toType(CurrentColors.value.base), null, null))
|
||||
}
|
||||
|
||||
fun currentColors(themeOverridesForType: WallpaperType?, perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverrides?, appSettingsTheme: List<ThemeOverrides>): ActiveTheme {
|
||||
|
||||
+2
-8
@@ -125,20 +125,14 @@ fun TerminalLayout(
|
||||
}
|
||||
}
|
||||
|
||||
private var lazyListState = 0 to 0
|
||||
|
||||
@Composable
|
||||
fun TerminalLog() {
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||
}
|
||||
val reversedTerminalItems by remember {
|
||||
derivedStateOf { chatModel.terminalItems.value.asReversed() }
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
LazyColumnWithScrollBar(state = listState, reverseLayout = true) {
|
||||
items(reversedTerminalItems) { item ->
|
||||
LazyColumnWithScrollBar(reverseLayout = true) {
|
||||
items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item ->
|
||||
val rhId = item.remoteHostId
|
||||
val rhIdStr = if (rhId == null) "" else "$rhId "
|
||||
Text(
|
||||
|
||||
+51
-45
@@ -111,53 +111,59 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) {
|
||||
val scrollState = rememberScrollState()
|
||||
val keyboardState by getKeyboardState()
|
||||
var savedKeyboardState by remember { mutableStateOf(keyboardState) }
|
||||
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.themedBackground(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
CloseSheetBar(close = {
|
||||
if (chatModel.users.none { !it.user.hidden }) {
|
||||
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
})
|
||||
BackHandler(onBack = {
|
||||
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||
})
|
||||
|
||||
ColumnWithScrollBar(
|
||||
modifier = if (appPlatform.isAndroid) Modifier.fillMaxSize() else Modifier.widthIn(max = 600.dp).fillMaxHeight(),
|
||||
val handler = remember { AppBarHandler() }
|
||||
CompositionLocalProvider(
|
||||
LocalAppBarHandler provides handler
|
||||
) {
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.themedBackground(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val displayName = rememberSaveable { mutableStateOf("") }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||
AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING)
|
||||
ProfileNameField(displayName, stringResource(MR.strings.display_name), { it.trim() == mkValidName(it) }, focusRequester)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Start, padding = PaddingValues(), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary))
|
||||
ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Start, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary))
|
||||
}
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
OnboardingActionButton(
|
||||
if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
|
||||
labelId = MR.strings.create_profile_button,
|
||||
onboarding = null,
|
||||
enabled = canCreateProfile(displayName.value),
|
||||
onclick = { createProfileOnboarding(chat.simplex.common.platform.chatModel, displayName.value, close) }
|
||||
)
|
||||
// Reserve space
|
||||
TextButtonBelowOnboardingButton("", null)
|
||||
}
|
||||
CloseSheetBar(close = {
|
||||
if (chatModel.users.none { !it.user.hidden }) {
|
||||
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
})
|
||||
BackHandler(onBack = {
|
||||
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||
})
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
delay(300)
|
||||
focusRequester.requestFocus()
|
||||
ColumnWithScrollBar(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
val displayName = rememberSaveable { mutableStateOf("") }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
Column(if (appPlatform.isAndroid) Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING) else Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally)) {
|
||||
Box(Modifier.align(Alignment.CenterHorizontally)) {
|
||||
AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING, withPadding = false)
|
||||
}
|
||||
ProfileNameField(displayName, stringResource(MR.strings.display_name), { it.trim() == mkValidName(it) }, focusRequester)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Start, padding = PaddingValues(), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary))
|
||||
ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Start, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary))
|
||||
}
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
OnboardingActionButton(
|
||||
if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
|
||||
labelId = MR.strings.create_profile_button,
|
||||
onboarding = null,
|
||||
enabled = canCreateProfile(displayName.value),
|
||||
onclick = { createProfileOnboarding(chat.simplex.common.platform.chatModel, displayName.value, close) }
|
||||
)
|
||||
// Reserve space
|
||||
TextButtonBelowOnboardingButton("", null)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
delay(300)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
|
||||
+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) {
|
||||
|
||||
+5
-5
@@ -599,7 +599,7 @@ fun ChatInfoLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
|
||||
val conn = contact.activeConn
|
||||
if (conn != null) {
|
||||
@@ -616,7 +616,7 @@ fun ChatInfoLayout(
|
||||
ShareAddressButton { clipboard.shareText(simplexChatLink(contact.contactLink)) }
|
||||
SectionTextFooter(stringResource(MR.strings.you_can_share_this_address_with_your_contacts).format(contact.displayName))
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
}
|
||||
|
||||
if (contact.ready && contact.active) {
|
||||
@@ -650,7 +650,7 @@ fun ChatInfoLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
}
|
||||
|
||||
SectionView {
|
||||
@@ -970,7 +970,7 @@ fun InfoViewActionButton(
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
Modifier.size(24.dp * fontSizeSqrtMultiplier),
|
||||
Modifier.size(22.dp * fontSizeSqrtMultiplier),
|
||||
tint = if (disabledLook) MaterialTheme.colors.secondary else MaterialTheme.colors.onPrimary
|
||||
)
|
||||
}
|
||||
@@ -1268,7 +1268,7 @@ fun showSyncConnectionForceAlert(syncConnectionForce: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
fun queueInfoText(info: Pair<RcvMsgInfo?, QueueInfo>): String {
|
||||
fun queueInfoText(info: Pair<RcvMsgInfo?, ServerQueueInfo>): String {
|
||||
val (rcvMsgInfo, qInfo) = info
|
||||
val msgInfo: String = if (rcvMsgInfo != null) json.encodeToString(rcvMsgInfo) else generalGetString(MR.strings.message_queue_info_none)
|
||||
return generalGetString(MR.strings.message_queue_info_server_info).format(json.encodeToString(qInfo), msgInfo)
|
||||
|
||||
+6
-10
@@ -224,7 +224,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.sizeIn(minHeight = 46.dp)
|
||||
.sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT)
|
||||
.padding(PaddingValues(horizontal = DEFAULT_PADDING))
|
||||
.clickable { expanded.value = !expanded.value },
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
@@ -277,20 +277,19 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
|
||||
@Composable
|
||||
fun HistoryTab() {
|
||||
// LALAL SCROLLBAR DOESN'T WORK
|
||||
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
|
||||
Details()
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
||||
val versions = ciInfo.itemVersions
|
||||
if (versions.isNotEmpty()) {
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(stringResource(MR.strings.edit_history), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
versions.forEachIndexed { i, ciVersion ->
|
||||
ItemVersionView(ciVersion, current = i == 0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(stringResource(MR.strings.no_history), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
@@ -302,11 +301,10 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
|
||||
@Composable
|
||||
fun QuoteTab(qi: CIQuote) {
|
||||
// LALAL SCROLLBAR DOESN'T WORK
|
||||
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
|
||||
Details()
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(stringResource(MR.strings.in_reply_to), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
QuotedMsgView(qi)
|
||||
}
|
||||
@@ -316,7 +314,6 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
|
||||
@Composable
|
||||
fun ForwardedFromTab(forwardedFromItem: AChatItem) {
|
||||
// LALAL SCROLLBAR DOESN'T WORK
|
||||
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
|
||||
Details()
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
||||
@@ -379,20 +376,19 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
|
||||
@Composable
|
||||
fun DeliveryTab(memberDeliveryStatuses: List<MemberDeliveryStatus>) {
|
||||
// LALAL SCROLLBAR DOESN'T WORK
|
||||
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
|
||||
Details()
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
||||
val mss = membersStatuses(chatModel, memberDeliveryStatuses)
|
||||
if (mss.isNotEmpty()) {
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(stringResource(MR.strings.delivery), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
mss.forEach { (member, status, sentViaProxy) ->
|
||||
MemberDeliveryStatusView(member, status, sentViaProxy)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(stringResource(MR.strings.no_info_on_delivery), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
|
||||
+34
-17
@@ -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) {
|
||||
@@ -504,24 +511,34 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
}
|
||||
is ChatInfo.ContactConnection -> {
|
||||
val close = { chatModel.chatId.value = null }
|
||||
ModalView(close, showClose = appPlatform.isAndroid, content = {
|
||||
ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, false, close)
|
||||
})
|
||||
LaunchedEffect(chatInfo.id) {
|
||||
onComposed(chatInfo.id)
|
||||
ModalManager.end.closeModals()
|
||||
chatModel.chatItems.clear()
|
||||
val handler = remember { AppBarHandler() }
|
||||
CompositionLocalProvider(
|
||||
LocalAppBarHandler provides handler
|
||||
) {
|
||||
ModalView(close, showClose = appPlatform.isAndroid, content = {
|
||||
ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, false, close)
|
||||
})
|
||||
LaunchedEffect(chatInfo.id) {
|
||||
onComposed(chatInfo.id)
|
||||
ModalManager.end.closeModals()
|
||||
chatModel.chatItems.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
is ChatInfo.InvalidJSON -> {
|
||||
val close = { chatModel.chatId.value = null }
|
||||
ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chatInfo.json) } }, content = {
|
||||
InvalidJSONView(chatInfo.json)
|
||||
})
|
||||
LaunchedEffect(chatInfo.id) {
|
||||
onComposed(chatInfo.id)
|
||||
ModalManager.end.closeModals()
|
||||
chatModel.chatItems.clear()
|
||||
val handler = remember { AppBarHandler() }
|
||||
CompositionLocalProvider(
|
||||
LocalAppBarHandler provides handler
|
||||
) {
|
||||
ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chatInfo.json) } }, content = {
|
||||
InvalidJSONView(chatInfo.json)
|
||||
})
|
||||
LaunchedEffect(chatInfo.id) {
|
||||
onComposed(chatInfo.id)
|
||||
ModalManager.end.closeModals()
|
||||
chatModel.chatItems.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
@@ -534,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))
|
||||
}
|
||||
@@ -987,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
|
||||
|
||||
+4
-4
@@ -93,22 +93,22 @@ private fun ContactPreferencesLayout(
|
||||
TimedMessagesFeatureSection(featuresAllowed, contact.mergedPreferences.timedMessages, timedMessages, onTTLUpdated) { allowed, ttl ->
|
||||
applyPrefs(featuresAllowed.copy(timedMessagesAllowed = allowed, timedMessagesTTL = ttl ?: currentFeaturesAllowed.timedMessagesTTL))
|
||||
}
|
||||
SectionDividerSpaced(true, maxBottomPadding = false)
|
||||
SectionDividerSpaced(true)
|
||||
val allowFullDeletion: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.fullDelete) }
|
||||
FeatureSection(ChatFeature.FullDelete, user.fullPreferences.fullDelete.allow, contact.mergedPreferences.fullDelete, allowFullDeletion) {
|
||||
applyPrefs(featuresAllowed.copy(fullDelete = it))
|
||||
}
|
||||
SectionDividerSpaced(true, maxBottomPadding = false)
|
||||
SectionDividerSpaced(true)
|
||||
val allowReactions: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.reactions) }
|
||||
FeatureSection(ChatFeature.Reactions, user.fullPreferences.reactions.allow, contact.mergedPreferences.reactions, allowReactions) {
|
||||
applyPrefs(featuresAllowed.copy(reactions = it))
|
||||
}
|
||||
SectionDividerSpaced(true, maxBottomPadding = false)
|
||||
SectionDividerSpaced(true)
|
||||
val allowVoice: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.voice) }
|
||||
FeatureSection(ChatFeature.Voice, user.fullPreferences.voice.allow, contact.mergedPreferences.voice, allowVoice) {
|
||||
applyPrefs(featuresAllowed.copy(voice = it))
|
||||
}
|
||||
SectionDividerSpaced(true, maxBottomPadding = false)
|
||||
SectionDividerSpaced(true)
|
||||
val allowCalls: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.calls) }
|
||||
FeatureSection(ChatFeature.Calls, user.fullPreferences.calls.allow, contact.mergedPreferences.calls, allowCalls) {
|
||||
applyPrefs(featuresAllowed.copy(calls = it))
|
||||
|
||||
+3
-3
@@ -66,7 +66,7 @@ fun SelectedItemsBottomToolbar(
|
||||
Icon(
|
||||
painterResource(MR.images.ic_delete),
|
||||
null,
|
||||
Modifier.size(24.dp),
|
||||
Modifier.size(22.dp),
|
||||
tint = if (!deleteEnabled.value || allButtonsDisabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
@@ -75,7 +75,7 @@ fun SelectedItemsBottomToolbar(
|
||||
Icon(
|
||||
painterResource(MR.images.ic_flag),
|
||||
null,
|
||||
Modifier.size(24.dp),
|
||||
Modifier.size(22.dp),
|
||||
tint = if (!moderateEnabled.value || allButtonsDisabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
@@ -84,7 +84,7 @@ fun SelectedItemsBottomToolbar(
|
||||
Icon(
|
||||
painterResource(MR.images.ic_share),
|
||||
null,
|
||||
Modifier.size(24.dp),
|
||||
Modifier.size(22.dp),
|
||||
tint = if (allButtonsDisabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
|
||||
+9
-5
@@ -4,6 +4,7 @@ import SectionBottomSpacer
|
||||
import SectionCustomFooter
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionItemViewWithoutMinPadding
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
@@ -177,7 +178,7 @@ fun AddGroupMembersLayout(
|
||||
InviteSectionFooter(selectedContactsCount = selectedContacts.size, allowModifyMembers, clearSelection)
|
||||
}
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
SectionView(stringResource(MR.strings.select_contacts)) {
|
||||
SectionView(stringResource(MR.strings.select_contacts).uppercase()) {
|
||||
SectionItemView(padding = PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF)) {
|
||||
SearchRowView(searchText)
|
||||
}
|
||||
@@ -255,7 +256,8 @@ fun InviteSectionFooter(selectedContactsCount: Int, enabled: Boolean, clearSelec
|
||||
Text(
|
||||
String.format(generalGetString(MR.strings.num_contacts_selected), selectedContactsCount),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
fontSize = 12.sp
|
||||
lineHeight = 18.sp,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
Box(
|
||||
Modifier.clickable { if (enabled) clearSelection() }
|
||||
@@ -263,14 +265,16 @@ fun InviteSectionFooter(selectedContactsCount: Int, enabled: Boolean, clearSelec
|
||||
Text(
|
||||
stringResource(MR.strings.clear_contacts_selection_button),
|
||||
color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
fontSize = 12.sp
|
||||
lineHeight = 18.sp,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
stringResource(MR.strings.no_contacts_selected),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
fontSize = 12.sp
|
||||
lineHeight = 18.sp,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -318,7 +322,7 @@ fun ContactCheckRow(
|
||||
icon = painterResource(MR.images.ic_circle)
|
||||
iconColor = MaterialTheme.colors.secondary
|
||||
}
|
||||
SectionItemView(
|
||||
SectionItemViewWithoutMinPadding(
|
||||
click = if (enabled) {
|
||||
{
|
||||
if (prohibitedToInviteIncognito) {
|
||||
|
||||
+9
-11
@@ -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,19 +271,18 @@ 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()
|
||||
if (s.isEmpty()) members else members.filter { m -> m.anyNameContains(s) }
|
||||
}
|
||||
}
|
||||
// LALAL strange scrolling
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
@@ -366,7 +364,7 @@ fun GroupChatInfoLayout(
|
||||
SearchRowView(searchText)
|
||||
}
|
||||
}
|
||||
SectionItemView(minHeight = 54.dp) {
|
||||
SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
MemberRow(groupInfo.membership, user = true)
|
||||
}
|
||||
}
|
||||
@@ -374,7 +372,7 @@ fun GroupChatInfoLayout(
|
||||
items(filteredMembers.value) { member ->
|
||||
Divider()
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
SectionItemViewLongClickable({ showMemberInfo(member) }, { showMenu.value = true }, minHeight = 54.dp) {
|
||||
SectionItemViewLongClickable({ showMemberInfo(member) }, { showMenu.value = true }, minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
DropDownMenuForMember(chat.remoteHostId, member, groupInfo, showMenu)
|
||||
MemberRow(member, onClick = { showMemberInfo(member) })
|
||||
}
|
||||
@@ -510,11 +508,11 @@ private fun MemberRow(member: GroupMember, user: Boolean = false, onClick: (() -
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
Modifier.weight(1f).padding(end = DEFAULT_PADDING),
|
||||
Modifier.weight(1f).padding(top = 8.dp, end = DEFAULT_PADDING, bottom = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
MemberProfileImage(size = 46.dp, member)
|
||||
MemberProfileImage(size = 42.dp, member)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING_HALF))
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -675,7 +673,7 @@ private fun SearchRowView(
|
||||
@Composable
|
||||
fun PreviewGroupChatInfoLayout() {
|
||||
SimpleXTheme {
|
||||
GroupChatInfoLayout(
|
||||
ModalData().GroupChatInfoLayout(
|
||||
chat = Chat(
|
||||
remoteHostId = null,
|
||||
chatInfo = ChatInfo.Direct.sampleData,
|
||||
|
||||
+1
-7
@@ -133,13 +133,7 @@ private fun GroupWelcomeLayout(
|
||||
val clipboard = LocalClipboardManager.current
|
||||
CopyTextButton { clipboard.setText(AnnotatedString(wt.value)) }
|
||||
|
||||
Divider(
|
||||
Modifier.padding(
|
||||
start = DEFAULT_PADDING_HALF,
|
||||
top = 8.dp,
|
||||
end = DEFAULT_PADDING_HALF,
|
||||
bottom = 8.dp)
|
||||
)
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
|
||||
SaveButton(
|
||||
save = save,
|
||||
|
||||
+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) }
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user